opencode-effect-enforcer 0.2.0
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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-platform-abstraction
|
|
3
|
+
description: Use Effect platform abstractions for cross-platform file I/O, process spawning, HTTP clients, cryptography, and terminal I/O. Apply when writing filesystem/process/HTTP/crypto/console code that must stay portable across Node.js, Bun, and browser adapters.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Platform Abstraction with Effect
|
|
7
|
+
|
|
8
|
+
## Effect Source Reference
|
|
9
|
+
|
|
10
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
12
|
+
|
|
13
|
+
Reference this for:
|
|
14
|
+
|
|
15
|
+
- FileSystem source: `packages/effect/src/FileSystem.ts`
|
|
16
|
+
- Path source: `packages/effect/src/Path.ts`
|
|
17
|
+
- Crypto source: `packages/effect/src/Crypto.ts`
|
|
18
|
+
- Socket source: `packages/effect/src/unstable/socket/`
|
|
19
|
+
- Platform layers: `packages/platform-node/`, `packages/platform-bun/`, and `packages/platform-browser/`
|
|
20
|
+
- Migration guide: `MIGRATION.md`
|
|
21
|
+
- Effect source: `packages/effect/src/`
|
|
22
|
+
|
|
23
|
+
## Overview
|
|
24
|
+
|
|
25
|
+
Effect provides platform-independent abstractions with Node.js and Bun adapters, plus browser adapters for supported services such as HTTP and Crypto. Instead of using runtime-specific APIs directly, you write code once using Effect Platform services and provide the appropriate layer at the edge.
|
|
26
|
+
|
|
27
|
+
**When to use this skill:**
|
|
28
|
+
|
|
29
|
+
- Writing file system operations
|
|
30
|
+
- Spawning child processes or executing commands
|
|
31
|
+
- Making HTTP requests
|
|
32
|
+
- Generating cryptographic random bytes, UUIDs, or digests
|
|
33
|
+
- Reading CLI arguments or environment variables
|
|
34
|
+
- Performing console/terminal I/O
|
|
35
|
+
- Working with paths across different operating systems
|
|
36
|
+
- Building cross-platform applications or libraries
|
|
37
|
+
|
|
38
|
+
## Why Effect Platform?
|
|
39
|
+
|
|
40
|
+
### 1. Cross-Platform Compatibility
|
|
41
|
+
|
|
42
|
+
Write once, run anywhere:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { Effect, FileSystem } from 'effect';
|
|
46
|
+
|
|
47
|
+
// Works on Node.js and Bun
|
|
48
|
+
const readConfig = Effect.gen(function* () {
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
return yield* fs.readFileString('config.json');
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Type-Safe Error Handling
|
|
55
|
+
|
|
56
|
+
All operations track errors in the Effect type signature:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Effect, FileSystem } from 'effect';
|
|
60
|
+
|
|
61
|
+
// Effect<string, PlatformError, FileSystem>
|
|
62
|
+
// ↓ ↓ ↓
|
|
63
|
+
// Required service
|
|
64
|
+
// Typed error channel
|
|
65
|
+
// Success value
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Resource Safety
|
|
69
|
+
|
|
70
|
+
Automatic cleanup with `Scope`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { Effect, FileSystem } from 'effect';
|
|
74
|
+
|
|
75
|
+
const program = Effect.gen(function* () {
|
|
76
|
+
const fs = yield* FileSystem.FileSystem;
|
|
77
|
+
// Read entire file into memory
|
|
78
|
+
return yield* fs.readFile('data.txt');
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4. Testability
|
|
83
|
+
|
|
84
|
+
Easy to mock and stub services:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { Effect, FileSystem, Layer } from 'effect';
|
|
88
|
+
|
|
89
|
+
declare const myProgram: Effect.Effect<void, never, FileSystem.FileSystem>;
|
|
90
|
+
|
|
91
|
+
const TestFileSystem = Layer.succeed(
|
|
92
|
+
FileSystem.FileSystem,
|
|
93
|
+
FileSystem.make({
|
|
94
|
+
readFile: () => Effect.succeed(new Uint8Array())
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const test = myProgram.pipe(Effect.provide(TestFileSystem));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5. Composability
|
|
102
|
+
|
|
103
|
+
Integrates naturally with Effect's service system:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { Effect, FileSystem, Layer, Path, Context } from 'effect';
|
|
107
|
+
|
|
108
|
+
interface ConfigService {
|
|
109
|
+
readonly load: (name: string) => Effect.Effect<string>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ConfigService = Context.Service<ConfigService>('ConfigService');
|
|
113
|
+
|
|
114
|
+
const ConfigServiceLive = Layer.effect(
|
|
115
|
+
ConfigService,
|
|
116
|
+
Effect.gen(function* () {
|
|
117
|
+
const fs = yield* FileSystem.FileSystem;
|
|
118
|
+
const path = yield* Path.Path;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
load: (name: string) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
const configPath = path.join('configs', name);
|
|
124
|
+
return yield* fs.readFileString(configPath);
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Core Platform Modules
|
|
132
|
+
|
|
133
|
+
### FileSystem - File Operations
|
|
134
|
+
|
|
135
|
+
The `FileSystem` service provides comprehensive file and directory operations.
|
|
136
|
+
|
|
137
|
+
**Anti-Pattern - Direct Node/Bun APIs:**
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// ❌ WRONG - Platform-specific, not testable
|
|
141
|
+
import * as fs from 'fs';
|
|
142
|
+
import { readFile } from 'fs/promises';
|
|
143
|
+
|
|
144
|
+
const content = fs.readFileSync('file.txt', 'utf-8');
|
|
145
|
+
const asyncContent = await readFile('file.txt', 'utf-8');
|
|
146
|
+
|
|
147
|
+
// ❌ WRONG - Bun-specific
|
|
148
|
+
declare const Bun: {
|
|
149
|
+
file: (path: string) => { text: () => Promise<string> };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const file = Bun.file('file.txt');
|
|
153
|
+
const content = await file.text();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Correct Pattern - FileSystem Service:**
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { Effect, FileSystem } from 'effect';
|
|
160
|
+
|
|
161
|
+
// ✅ CORRECT - Cross-platform, type-safe, testable
|
|
162
|
+
const readFile = (path: string) =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const fs = yield* FileSystem.FileSystem;
|
|
165
|
+
return yield* fs.readFileString(path);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Effect<string, PlatformError, FileSystem>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Common Operations:**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { Effect, FileSystem } from 'effect';
|
|
175
|
+
|
|
176
|
+
const fileOperations = Effect.gen(function* () {
|
|
177
|
+
const fs = yield* FileSystem.FileSystem;
|
|
178
|
+
|
|
179
|
+
// Read files
|
|
180
|
+
const text = yield* fs.readFileString('data.txt');
|
|
181
|
+
const bytes = yield* fs.readFile('binary.dat');
|
|
182
|
+
|
|
183
|
+
// Write files
|
|
184
|
+
yield* fs.writeFileString('output.txt', 'Hello World');
|
|
185
|
+
|
|
186
|
+
// Directory operations
|
|
187
|
+
yield* fs.makeDirectory('new-dir', { recursive: true });
|
|
188
|
+
const files = yield* fs.readDirectory('src');
|
|
189
|
+
|
|
190
|
+
// File metadata
|
|
191
|
+
const stats = yield* fs.stat('file.txt');
|
|
192
|
+
const exists = yield* fs.exists('config.json');
|
|
193
|
+
|
|
194
|
+
// Copy and move
|
|
195
|
+
yield* fs.copy('source.txt', 'dest.txt');
|
|
196
|
+
yield* fs.rename('old.txt', 'new.txt');
|
|
197
|
+
|
|
198
|
+
// Remove files/directories
|
|
199
|
+
yield* fs.remove('temp-file.txt');
|
|
200
|
+
yield* fs.remove('temp-dir', { recursive: true });
|
|
201
|
+
|
|
202
|
+
// Temporary files (auto-cleanup with Scope)
|
|
203
|
+
const tempFile = yield* fs.makeTempFileScoped();
|
|
204
|
+
yield* fs.writeFileString(tempFile, 'temporary data');
|
|
205
|
+
// File automatically deleted when scope closes
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`fs.watch(directory)` reports direct-child changes by default; pass `{ recursive: true }` to include nested subdirectories. For open handles, `file.seek(offset, 'start' | 'current')` returns the new offset as a branded `FileSystem.Size`. The old `FileSystem.File.Descriptor` type and `file.descriptor` property were removed in beta.103; use scoped `File` methods instead.
|
|
210
|
+
|
|
211
|
+
**Streaming Files:**
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { Effect, FileSystem, Stream } from 'effect';
|
|
215
|
+
|
|
216
|
+
declare const processChunk: (chunk: Uint8Array) => Effect.Effect<void>;
|
|
217
|
+
|
|
218
|
+
// Stream large files efficiently
|
|
219
|
+
const processLargeFile = Effect.gen(function* () {
|
|
220
|
+
const fs = yield* FileSystem.FileSystem;
|
|
221
|
+
|
|
222
|
+
// Read as stream
|
|
223
|
+
const stream = fs.stream('large-file.txt', { chunkSize: 64 * 1024 });
|
|
224
|
+
|
|
225
|
+
// Process stream
|
|
226
|
+
yield* stream.pipe(
|
|
227
|
+
Stream.mapEffect((chunk) => processChunk(chunk)),
|
|
228
|
+
Stream.run(fs.sink('output.txt'))
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Path - Path Manipulation
|
|
234
|
+
|
|
235
|
+
The `Path` service provides cross-platform path operations.
|
|
236
|
+
|
|
237
|
+
**Anti-Pattern - Manual String Manipulation:**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// ❌ WRONG - Breaks on Windows, brittle
|
|
241
|
+
import path from 'path';
|
|
242
|
+
|
|
243
|
+
declare const process: { cwd: () => string };
|
|
244
|
+
declare const filename: string;
|
|
245
|
+
|
|
246
|
+
const configPath = './config/' + filename + '.json';
|
|
247
|
+
const absPath = process.cwd() + '/' + configPath;
|
|
248
|
+
|
|
249
|
+
// ❌ WRONG - Node-specific
|
|
250
|
+
const joined = path.join('src', 'components', 'Button.tsx');
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Correct Pattern - Path Service:**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { Effect, Path } from 'effect';
|
|
257
|
+
|
|
258
|
+
// ✅ CORRECT - Cross-platform path handling
|
|
259
|
+
const buildPath = (filename: string) =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
const path = yield* Path.Path;
|
|
262
|
+
|
|
263
|
+
// Join paths correctly for any OS
|
|
264
|
+
const configPath = path.join('config', `${filename}.json`);
|
|
265
|
+
|
|
266
|
+
// Resolve to absolute path
|
|
267
|
+
const absolutePath = path.resolve(configPath);
|
|
268
|
+
|
|
269
|
+
// Extract path components
|
|
270
|
+
const dir = path.dirname(absolutePath);
|
|
271
|
+
const base = path.basename(absolutePath);
|
|
272
|
+
const ext = path.extname(absolutePath);
|
|
273
|
+
|
|
274
|
+
// Parse path into components
|
|
275
|
+
const parsed = path.parse(absolutePath);
|
|
276
|
+
// { root, dir, base, ext, name }
|
|
277
|
+
|
|
278
|
+
return absolutePath;
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Path Operations:**
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { Effect, Path } from 'effect';
|
|
286
|
+
|
|
287
|
+
const pathOps = Effect.gen(function* () {
|
|
288
|
+
const path = yield* Path.Path;
|
|
289
|
+
|
|
290
|
+
// Platform-specific separator ("/" or "\")
|
|
291
|
+
const sep = path.sep;
|
|
292
|
+
|
|
293
|
+
// Join multiple segments
|
|
294
|
+
const filePath = path.join('src', 'lib', 'utils.ts');
|
|
295
|
+
|
|
296
|
+
// Resolve relative paths
|
|
297
|
+
const absolute = path.resolve('..', 'config', 'app.json');
|
|
298
|
+
|
|
299
|
+
// Get relative path between two paths
|
|
300
|
+
const rel = path.relative('/app/src', '/app/dist');
|
|
301
|
+
|
|
302
|
+
// Check if path is absolute
|
|
303
|
+
const isAbs = path.isAbsolute('/usr/local');
|
|
304
|
+
|
|
305
|
+
// Normalize path (remove "..", ".", etc.)
|
|
306
|
+
const normalized = path.normalize('src/../lib/./utils.ts');
|
|
307
|
+
|
|
308
|
+
// Work with file URLs
|
|
309
|
+
const url = yield* path.toFileUrl('/path/to/file');
|
|
310
|
+
const fromUrl = yield* path.fromFileUrl(new URL('file:///path/to/file'));
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
In Effect v4, `Migrator.fromFileSystem` requires both `FileSystem.FileSystem` and `Path.Path` because migration module paths are converted to file URLs before dynamic import. Aggregate Node/Bun service layers satisfy both requirements. When providing services individually on Windows, use the host-aware path layer rather than core `Path.layer`, which has POSIX semantics.
|
|
315
|
+
|
|
316
|
+
### ChildProcess - Process Execution
|
|
317
|
+
|
|
318
|
+
The `ChildProcess` and `ChildProcessSpawner` services enable safe process spawning.
|
|
319
|
+
|
|
320
|
+
**Anti-Pattern - Direct child_process:**
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// ❌ WRONG - Node-specific, no resource safety
|
|
324
|
+
import { spawn, exec } from 'child_process';
|
|
325
|
+
import { promisify } from 'util';
|
|
326
|
+
|
|
327
|
+
const execAsync = promisify(exec);
|
|
328
|
+
const { stdout } = await execAsync('ls -la');
|
|
329
|
+
|
|
330
|
+
// ❌ WRONG - Bun-specific
|
|
331
|
+
declare const Bun: {
|
|
332
|
+
spawn: (cmd: string[]) => { stdout: ReadableStream };
|
|
333
|
+
};
|
|
334
|
+
declare const Response: {
|
|
335
|
+
new (stream: ReadableStream): { text: () => Promise<string> };
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const proc = Bun.spawn(['ls', '-la']);
|
|
339
|
+
const output = await new Response(proc.stdout).text();
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Correct Pattern - ChildProcess + ChildProcessSpawner:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
346
|
+
import { Effect, Stream } from 'effect';
|
|
347
|
+
|
|
348
|
+
// ✅ CORRECT - Cross-platform command execution
|
|
349
|
+
const runCommand = Effect.gen(function* () {
|
|
350
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
351
|
+
|
|
352
|
+
// Create command and collect output as string
|
|
353
|
+
const output = yield* spawner.string(ChildProcess.make('ls', ['-la']));
|
|
354
|
+
|
|
355
|
+
return output;
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Advanced ChildProcess Usage:**
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
363
|
+
import { Console, Effect, Stream } from 'effect';
|
|
364
|
+
|
|
365
|
+
const commandExamples = Effect.gen(function* () {
|
|
366
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
367
|
+
|
|
368
|
+
// Collect output as string
|
|
369
|
+
const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));
|
|
370
|
+
|
|
371
|
+
// Collect output as lines
|
|
372
|
+
const lines = yield* spawner.lines(
|
|
373
|
+
ChildProcess.make('git', ['log', '--pretty=format:%s', '-n', '10'])
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Pipe commands together
|
|
377
|
+
const pipeline = ChildProcess.make('cat', ['file.txt']).pipe(
|
|
378
|
+
ChildProcess.pipeTo(ChildProcess.make('grep', ['error'])),
|
|
379
|
+
ChildProcess.pipeTo(ChildProcess.make('wc', ['-l']))
|
|
380
|
+
);
|
|
381
|
+
const pipelineOutput = yield* spawner.string(pipeline);
|
|
382
|
+
|
|
383
|
+
// Set environment variables
|
|
384
|
+
const withEnv = ChildProcess.make('node', ['script.js'], {
|
|
385
|
+
env: { NODE_ENV: 'production', API_KEY: 'secret' },
|
|
386
|
+
extendEnv: true
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Spawn process and stream output
|
|
390
|
+
const handle = yield* spawner.spawn(
|
|
391
|
+
ChildProcess.make('npm', ['run', 'build'])
|
|
392
|
+
);
|
|
393
|
+
yield* handle.all.pipe(
|
|
394
|
+
Stream.decodeText(),
|
|
395
|
+
Stream.splitLines,
|
|
396
|
+
Stream.runForEach((line) => Console.log(`[build] ${line}`))
|
|
397
|
+
);
|
|
398
|
+
const exitCode = yield* handle.exitCode;
|
|
399
|
+
|
|
400
|
+
return exitCode;
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Terminal - Terminal I/O
|
|
405
|
+
|
|
406
|
+
The `Terminal` service provides interactive terminal capabilities.
|
|
407
|
+
|
|
408
|
+
**Anti-Pattern - Direct Console:**
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// ❌ WRONG - Uses global console, not testable
|
|
412
|
+
declare const console: {
|
|
413
|
+
log: (msg: string) => void;
|
|
414
|
+
error: (msg: string) => void;
|
|
415
|
+
};
|
|
416
|
+
declare const process: {
|
|
417
|
+
stdout: { write: (msg: string) => void };
|
|
418
|
+
};
|
|
419
|
+
declare const prompt: (msg: string) => string | null;
|
|
420
|
+
|
|
421
|
+
console.log('Hello World');
|
|
422
|
+
console.error('Error occurred');
|
|
423
|
+
process.stdout.write('Output\n');
|
|
424
|
+
|
|
425
|
+
// ❌ WRONG - Not trackable in Effect type
|
|
426
|
+
const input = prompt('Enter name:');
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Correct Pattern - Terminal Service:**
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { Effect, Terminal } from 'effect';
|
|
433
|
+
|
|
434
|
+
// ✅ CORRECT - Trackable, testable terminal I/O
|
|
435
|
+
const interactiveProgram = Effect.gen(function* () {
|
|
436
|
+
const terminal = yield* Terminal.Terminal;
|
|
437
|
+
|
|
438
|
+
// Display output
|
|
439
|
+
yield* terminal.display('Hello World\n');
|
|
440
|
+
|
|
441
|
+
// Read user input
|
|
442
|
+
const name = yield* terminal.readLine;
|
|
443
|
+
yield* terminal.display(`Welcome, ${name}!\n`);
|
|
444
|
+
|
|
445
|
+
// Get terminal dimensions
|
|
446
|
+
const cols = yield* terminal.columns;
|
|
447
|
+
const rows = yield* terminal.rows;
|
|
448
|
+
yield* terminal.display(`Terminal size: ${cols}x${rows}\n`);
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**For Simple Logging - Use Console or Effect.log:**
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { Console, Effect } from 'effect';
|
|
456
|
+
|
|
457
|
+
// ✅ CORRECT - Console service (from effect)
|
|
458
|
+
const logging = Effect.gen(function* () {
|
|
459
|
+
yield* Console.log('Info message');
|
|
460
|
+
yield* Console.error('Error message');
|
|
461
|
+
yield* Console.warn('Warning');
|
|
462
|
+
yield* Console.debug('Debug info');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ✅ CORRECT - Effect.log with structured logging
|
|
466
|
+
const structuredLog = Effect.gen(function* () {
|
|
467
|
+
yield* Effect.log('Operation started');
|
|
468
|
+
yield* Effect.logDebug('Debug details');
|
|
469
|
+
yield* Effect.logError('Error occurred');
|
|
470
|
+
|
|
471
|
+
// With annotations
|
|
472
|
+
yield* Effect.log('User action').pipe(
|
|
473
|
+
Effect.annotateLogs('userId', '123'),
|
|
474
|
+
Effect.annotateLogs('action', 'login')
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Crypto - Cryptographic Randomness, UUIDs, and Digests
|
|
480
|
+
|
|
481
|
+
The `Crypto.Crypto` service provides platform-backed cryptographic random bytes, UUIDv4/v7 generation, and message digests. Prefer it over `globalThis.crypto`, `crypto.randomUUID()`, or ad-hoc randomness when code should stay platform-abstract and testable.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { Crypto, Effect } from 'effect';
|
|
485
|
+
|
|
486
|
+
const cryptoProgram = Effect.gen(function* () {
|
|
487
|
+
const crypto = yield* Crypto.Crypto;
|
|
488
|
+
|
|
489
|
+
const bytes = yield* crypto.randomBytes(32);
|
|
490
|
+
const uuidV4 = yield* crypto.randomUUIDv4;
|
|
491
|
+
const uuidV7 = yield* crypto.randomUUIDv7;
|
|
492
|
+
const digest = yield* crypto.digest('SHA-256', bytes);
|
|
493
|
+
|
|
494
|
+
return { bytes, uuidV4, uuidV7, digest };
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`NodeServices.layer` and `BunServices.layer` include `Crypto.Crypto`. Browser applications can provide `BrowserCrypto.layer` from `@effect/platform-browser`.
|
|
499
|
+
|
|
500
|
+
### HttpClient - HTTP Requests
|
|
501
|
+
|
|
502
|
+
The `HttpClient` service provides type-safe HTTP operations. Runtime application and provider code must use it rather than raw `fetch`. Raw fetch is reserved for an explicitly named low-level platform adapter whose documentation justifies why an Effect transport cannot be used; that adapter must own interruption, status classification, schema decoding, and typed error mapping.
|
|
503
|
+
|
|
504
|
+
**Anti-Pattern - Direct fetch/axios:**
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
// ❌ WRONG - No Effect integration, manual error handling
|
|
508
|
+
declare const fetch: (url: string) => Promise<{ json: () => Promise<unknown> }>;
|
|
509
|
+
|
|
510
|
+
const response = await fetch('https://api.example.com/data');
|
|
511
|
+
const data = await response.json();
|
|
512
|
+
|
|
513
|
+
// ❌ WRONG - External dependency, not in Effect system
|
|
514
|
+
import axios from 'axios';
|
|
515
|
+
|
|
516
|
+
declare const axios: {
|
|
517
|
+
get: (url: string) => Promise<{ data: unknown }>;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const result = await axios.get('https://api.example.com/data');
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Correct Pattern - HttpClient Service:**
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import { HttpClient, HttpClientResponse } from 'effect/unstable/http';
|
|
527
|
+
import { Effect, Schema } from 'effect';
|
|
528
|
+
|
|
529
|
+
// ✅ CORRECT - Integrated with Effect type system
|
|
530
|
+
class ProviderData extends Schema.Class<ProviderData>('ProviderData')({
|
|
531
|
+
value: Schema.String
|
|
532
|
+
}) {}
|
|
533
|
+
|
|
534
|
+
const fetchData = Effect.gen(function* () {
|
|
535
|
+
const client = yield* HttpClient.HttpClient;
|
|
536
|
+
|
|
537
|
+
return yield* client.get('https://api.example.com/data').pipe(
|
|
538
|
+
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
539
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(ProviderData))
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Name the adapter service and its effects after the upstream operation. The adapter owns request/auth construction, executes outside database transactions, classifies status before decoding, validates unknown bodies with `Schema`, and maps failures into typed domain errors. Preserve bounded evidence such as status, provider error code, request ID, and retry metadata, but redact credentials, private fields, query secrets, and full response bodies.
|
|
545
|
+
|
|
546
|
+
**Advanced HTTP Operations:**
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import {
|
|
550
|
+
HttpClient,
|
|
551
|
+
HttpClientRequest,
|
|
552
|
+
HttpClientResponse
|
|
553
|
+
} from 'effect/unstable/http';
|
|
554
|
+
import { Effect, Schema, Schedule } from 'effect';
|
|
555
|
+
|
|
556
|
+
class User extends Schema.Class<User>('User')({
|
|
557
|
+
id: Schema.Number,
|
|
558
|
+
name: Schema.String,
|
|
559
|
+
email: Schema.String
|
|
560
|
+
}) {}
|
|
561
|
+
|
|
562
|
+
const httpExamples = Effect.gen(function* () {
|
|
563
|
+
const client = yield* HttpClient.HttpClient;
|
|
564
|
+
|
|
565
|
+
// GET with query parameters
|
|
566
|
+
const getUsers = client.get('https://api.example.com/users', {
|
|
567
|
+
urlParams: { page: '1', limit: '10' }
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// POST with JSON body
|
|
571
|
+
const createUser = HttpClientRequest.post(
|
|
572
|
+
'https://api.example.com/users'
|
|
573
|
+
).pipe(
|
|
574
|
+
HttpClientRequest.bodyJsonUnsafe({
|
|
575
|
+
name: 'John Doe',
|
|
576
|
+
email: 'john@example.com'
|
|
577
|
+
}),
|
|
578
|
+
client.execute
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// Custom headers — construct the request, set headers, then execute
|
|
582
|
+
const withAuthRequest = HttpClientRequest.get(
|
|
583
|
+
'https://api.example.com/protected'
|
|
584
|
+
).pipe(HttpClientRequest.setHeader('Authorization', 'Bearer token'));
|
|
585
|
+
const withAuth = client.execute(withAuthRequest);
|
|
586
|
+
|
|
587
|
+
// Classify status, then parse the successful response with Schema
|
|
588
|
+
const users = yield* client
|
|
589
|
+
.get('https://api.example.com/users')
|
|
590
|
+
.pipe(
|
|
591
|
+
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
592
|
+
Effect.flatMap(
|
|
593
|
+
HttpClientResponse.schemaBodyJson(Schema.Array(User))
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Error handling — all HttpClient errors are "HttpClientError" with a reason field
|
|
598
|
+
const safeRequest = client.get('https://api.example.com/data').pipe(
|
|
599
|
+
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
600
|
+
Effect.catchTag('HttpClientError', (error) => {
|
|
601
|
+
switch (error.reason._tag) {
|
|
602
|
+
case 'TransportError':
|
|
603
|
+
return Effect.succeed({ error: 'Network error' });
|
|
604
|
+
case 'StatusCodeError':
|
|
605
|
+
return Effect.succeed({
|
|
606
|
+
error: `HTTP ${error.response?.status}`
|
|
607
|
+
});
|
|
608
|
+
default:
|
|
609
|
+
return Effect.succeed({
|
|
610
|
+
error: `Client error: ${error.reason._tag}`
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Retries with backoff: GET is idempotent and attempts are bounded.
|
|
617
|
+
const withRetries = client.get('https://api.example.com/data').pipe(
|
|
618
|
+
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
619
|
+
Effect.retry({
|
|
620
|
+
times: 3,
|
|
621
|
+
schedule: Schedule.exponential('100 millis')
|
|
622
|
+
}),
|
|
623
|
+
Effect.tapError((error) =>
|
|
624
|
+
Effect.logError('Provider read exhausted retries').pipe(
|
|
625
|
+
Effect.annotateLogs({ operation: 'Provider.getData', errorTag: error._tag })
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
return users;
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Never install retry automatically on a shared client that also sends non-idempotent POST/PATCH requests. Retry only operations proven idempotent by method/provider contract or protected by a provider-supported idempotency key. Keep exhaustion observable: retain the final typed error and log/measure only redacted status, provider code, request ID, operation, and attempt evidence.
|
|
635
|
+
|
|
636
|
+
`HttpClient.withRateLimiter` can automatically retry 429 responses. Use positive `times` only on a client restricted to proven-idempotent operations; set `times: 0` for mixed/non-idempotent clients so the 429 remains visible.
|
|
637
|
+
|
|
638
|
+
### KeyValueStore - Key-Value Storage
|
|
639
|
+
|
|
640
|
+
The `KeyValueStore` service provides platform-independent key-value storage.
|
|
641
|
+
|
|
642
|
+
**Anti-Pattern - Direct localStorage/file-based storage:**
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// ❌ WRONG - Browser-specific
|
|
646
|
+
declare const localStorage: {
|
|
647
|
+
setItem: (key: string, value: string) => void;
|
|
648
|
+
getItem: (key: string) => string | null;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
localStorage.setItem('key', 'value');
|
|
652
|
+
const value = localStorage.getItem('key');
|
|
653
|
+
|
|
654
|
+
// ❌ WRONG - Node-specific, manual file handling
|
|
655
|
+
import fs from 'fs';
|
|
656
|
+
|
|
657
|
+
declare const fs: {
|
|
658
|
+
writeFileSync: (path: string, data: string) => void;
|
|
659
|
+
readFileSync: (path: string, encoding: string) => string;
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
fs.writeFileSync('.cache/key', 'value');
|
|
663
|
+
const value2 = fs.readFileSync('.cache/key', 'utf-8');
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Correct Pattern - KeyValueStore Service:**
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { KeyValueStore } from 'effect/unstable/persistence';
|
|
670
|
+
import { Effect, Schema } from 'effect';
|
|
671
|
+
|
|
672
|
+
// ✅ CORRECT - Works on all platforms
|
|
673
|
+
const cacheData = Effect.gen(function* () {
|
|
674
|
+
const store = yield* KeyValueStore.KeyValueStore;
|
|
675
|
+
|
|
676
|
+
// Set value
|
|
677
|
+
yield* store.set('user:123', 'John Doe');
|
|
678
|
+
|
|
679
|
+
// Get value — raw string stores return string | undefined
|
|
680
|
+
const name = yield* store.get('user:123');
|
|
681
|
+
|
|
682
|
+
// Binary stores return Uint8Array | undefined
|
|
683
|
+
const bytes = yield* store.getUint8Array('avatar:123');
|
|
684
|
+
|
|
685
|
+
// Check existence
|
|
686
|
+
const hasUser = yield* store.has('user:123');
|
|
687
|
+
|
|
688
|
+
// Remove value
|
|
689
|
+
yield* store.remove('user:123');
|
|
690
|
+
|
|
691
|
+
// Clear all
|
|
692
|
+
yield* store.clear;
|
|
693
|
+
|
|
694
|
+
return name;
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Schema-Based Store:**
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
import { KeyValueStore } from 'effect/unstable/persistence';
|
|
702
|
+
import { Effect, Schema } from 'effect';
|
|
703
|
+
|
|
704
|
+
class User extends Schema.Class<User>('User')({
|
|
705
|
+
id: Schema.Number,
|
|
706
|
+
name: Schema.String,
|
|
707
|
+
email: Schema.String
|
|
708
|
+
}) {}
|
|
709
|
+
|
|
710
|
+
const typedStore = Effect.gen(function* () {
|
|
711
|
+
const store = yield* KeyValueStore.KeyValueStore;
|
|
712
|
+
|
|
713
|
+
// Create schema-based store
|
|
714
|
+
const userStore = KeyValueStore.toSchemaStore(store, User);
|
|
715
|
+
|
|
716
|
+
// Type-safe operations
|
|
717
|
+
yield* userStore.set('user:123', new User({
|
|
718
|
+
id: 123,
|
|
719
|
+
name: 'John Doe',
|
|
720
|
+
email: 'john@example.com'
|
|
721
|
+
}));
|
|
722
|
+
|
|
723
|
+
const user = yield* userStore.get('user:123');
|
|
724
|
+
// user: Option.Option<{ id: number, name: string, email: string }>
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Redis - Commands and Scoped Subscriptions
|
|
729
|
+
|
|
730
|
+
The portable `Redis.Redis` service in `effect/unstable/persistence` provides `send`, cached script evaluation, and scoped pub/sub. Platform-specific layers provide the client: `NodeRedis.layer(...)`, `DenoRedis.layer(...)`, or `BunRedis.layer(...)`. These are specialized layers and are not included in `NodeServices.layer` or `BunServices.layer`.
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
import { NodeRedis } from '@effect/platform-node';
|
|
734
|
+
import { Effect, Queue } from 'effect';
|
|
735
|
+
import { Redis } from 'effect/unstable/persistence';
|
|
736
|
+
|
|
737
|
+
const RedisLayer = NodeRedis.layer({
|
|
738
|
+
database: 1,
|
|
739
|
+
socket: { host: '127.0.0.1', port: 6379 }
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const receiveOne = Effect.gen(function* () {
|
|
743
|
+
const redis = yield* Redis.Redis;
|
|
744
|
+
const subscription = yield* redis.subscribe('events');
|
|
745
|
+
return yield* Queue.take(subscription);
|
|
746
|
+
}).pipe(Effect.scoped, Effect.provide(RedisLayer));
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
`redis.subscribe(channel)` requires `Scope` and returns a `Queue.Dequeue<RedisMessage, RedisError>`. Closing the scope shuts down the queue and releases the dedicated subscriber. Node and Deno subscribers reconnect and re-subscribe after interruptions, which can leave message-delivery gaps; Bun subscriptions do not reconnect, so a dropped connection fails the dequeue and callers must subscribe again.
|
|
750
|
+
|
|
751
|
+
### CLI Arguments - effect/unstable/cli
|
|
752
|
+
|
|
753
|
+
For CLI applications, use `effect/unstable/cli` instead of direct `process.argv`.
|
|
754
|
+
|
|
755
|
+
**Anti-Pattern - Direct process.argv:**
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
// ❌ WRONG - Manual parsing, no validation
|
|
759
|
+
declare const process: { argv: string[] };
|
|
760
|
+
|
|
761
|
+
const args = process.argv.slice(2);
|
|
762
|
+
const input = args[0];
|
|
763
|
+
const verbose = args.includes('--verbose');
|
|
764
|
+
|
|
765
|
+
// ❌ WRONG - Third-party parser, not Effect-integrated
|
|
766
|
+
import yargs from 'yargs';
|
|
767
|
+
|
|
768
|
+
declare const yargs: (args: string[]) => { argv: Record<string, unknown> };
|
|
769
|
+
|
|
770
|
+
const argv = yargs(process.argv.slice(2)).argv;
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Correct Pattern - effect/unstable/cli:**
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import { Argument, Command as CliCommand, Flag } from 'effect/unstable/cli';
|
|
777
|
+
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
778
|
+
import { Console, Effect } from 'effect';
|
|
779
|
+
|
|
780
|
+
declare const process: { argv: string[] };
|
|
781
|
+
declare const someOperation: Effect.Effect<string>;
|
|
782
|
+
|
|
783
|
+
// ✅ CORRECT - Type-safe CLI with full Effect integration
|
|
784
|
+
// Define arguments
|
|
785
|
+
const inputArg = Argument.file('input');
|
|
786
|
+
|
|
787
|
+
// Define flags
|
|
788
|
+
const verboseFlag = Flag.boolean('verbose').pipe(Flag.withAlias('v'));
|
|
789
|
+
|
|
790
|
+
// Define command
|
|
791
|
+
const command = CliCommand.make(
|
|
792
|
+
'process',
|
|
793
|
+
{ input: inputArg, verbose: verboseFlag },
|
|
794
|
+
Effect.fn(function* ({ input, verbose }) {
|
|
795
|
+
if (verbose) {
|
|
796
|
+
yield* Console.log(`Processing file: ${input}`);
|
|
797
|
+
}
|
|
798
|
+
// Process the file
|
|
799
|
+
yield* someOperation;
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
// Run CLI
|
|
804
|
+
command.pipe(
|
|
805
|
+
CliCommand.run({ version: '1.0.0' }),
|
|
806
|
+
Effect.provide(NodeServices.layer),
|
|
807
|
+
NodeRuntime.runMain
|
|
808
|
+
);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
## Platform Module Reference
|
|
812
|
+
|
|
813
|
+
Complete reference table of platform abstractions:
|
|
814
|
+
|
|
815
|
+
| Need | Use | Instead of | Import from |
|
|
816
|
+
| ------------------------- | -------------------------------------- | ---------------------------- | ----------------------------- |
|
|
817
|
+
| **File I/O** | `FileSystem.FileSystem` | `fs`, `Bun.file` | `effect` |
|
|
818
|
+
| **Path Operations** | `Path.Path` | `path`, string concat | `effect` |
|
|
819
|
+
| **Process Spawning** | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/unstable/process` |
|
|
820
|
+
| **Terminal I/O** | `Terminal.Terminal` | `process.stdin/stdout` | `effect` |
|
|
821
|
+
| **Console Logging** | `Console.log` or `Effect.log` | `console.log` | `effect` |
|
|
822
|
+
| **Crypto** | `Crypto.Crypto` | `globalThis.crypto`, `crypto.randomUUID()` | `effect` |
|
|
823
|
+
| **HTTP Client** | `HttpClient.HttpClient` | `fetch`, `axios` | `effect/unstable/http` |
|
|
824
|
+
| **HTTP Server** | `HttpServer.HttpServer` | `http.createServer` | `effect/unstable/http` |
|
|
825
|
+
| **Sockets** | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/unstable/socket` |
|
|
826
|
+
| **Key-Value Store** | `KeyValueStore.KeyValueStore` | `localStorage`, manual files | `effect/unstable/persistence` |
|
|
827
|
+
| **Redis** | `Redis.Redis` | direct Redis clients | `effect/unstable/persistence` |
|
|
828
|
+
| **CLI Arguments** | `Argument` + `Flag` + `Command` | `process.argv`, `yargs` | `effect/unstable/cli` |
|
|
829
|
+
| **Environment Variables** | `Config` from effect | `process.env` | `effect` |
|
|
830
|
+
| **Streams** | `Stream` | Node streams, ReadableStream | `effect` |
|
|
831
|
+
|
|
832
|
+
Socket services live in `effect/unstable/socket`. Use `Socket.Socket` for scoped bidirectional string/binary frame transports and `SocketServer.SocketServer` for accepting connections. Provide service-specific layers such as `BrowserSocket.layerWebSocket(url)`, `NodeSocket.layerWebSocket(url)`, `NodeSocket.layerNet(options)`, `BunSocket.layerWebSocket(url)`, or Node/Bun socket-server layers; `NodeServices.layer` and `BunServices.layer` do not provide sockets.
|
|
833
|
+
|
|
834
|
+
## Setting Up Platform-Specific Layers
|
|
835
|
+
|
|
836
|
+
To use platform services, provide the appropriate platform layer.
|
|
837
|
+
|
|
838
|
+
`NodeServices.layer` and `BunServices.layer` provide core process services such as `FileSystem`, `Path`, `ChildProcessSpawner`, `Stdio`/`Terminal`, and `Crypto.Crypto`. They do **not** provide specialized integrations such as HTTP clients/servers, sockets, workers, or Redis; provide those with service-specific platform layers. For HTTP clients, provide an HTTP-specific layer such as `FetchHttpClient.layer`, Node's `NodeHttpClient.{layerFetch, layerUndici, layerNodeHttp}`, `BunHttpClient.layer`, or Browser's `BrowserHttpClient.{layerFetch, layerXMLHttpRequest}`. A named provider adapter should export a raw `layer` that requires `HttpClient.HttpClient`, plus an optional `defaultLayer = layer.pipe(Layer.provide(...transport...))`; this keeps the transport dependency graph explicit while offering runtime convenience. For HTTP servers, use server layers such as `NodeHttpServer.layer(...)`, `BunHttpServer.layer(...)`, or their `layerHttpServices` variants where appropriate.
|
|
839
|
+
|
|
840
|
+
**Node.js:**
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
844
|
+
import { Effect, FileSystem } from 'effect';
|
|
845
|
+
|
|
846
|
+
const program = Effect.gen(function* () {
|
|
847
|
+
const fs = yield* FileSystem.FileSystem;
|
|
848
|
+
return yield* fs.readFileString('file.txt');
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
**Bun:**
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
import { BunServices, BunRuntime } from '@effect/platform-bun';
|
|
858
|
+
import { Effect, FileSystem } from 'effect';
|
|
859
|
+
|
|
860
|
+
const program = Effect.gen(function* () {
|
|
861
|
+
const fs = yield* FileSystem.FileSystem;
|
|
862
|
+
return yield* fs.readFileString('file.txt');
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## Complete Example: Cross-Platform File Processor
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
872
|
+
import { BunServices, BunRuntime } from '@effect/platform-bun';
|
|
873
|
+
import { Console, Effect, FileSystem, Path, Schema } from 'effect';
|
|
874
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
875
|
+
|
|
876
|
+
class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
|
|
877
|
+
'FileProcessorConfig'
|
|
878
|
+
)({
|
|
879
|
+
inputDir: Schema.String,
|
|
880
|
+
outputDir: Schema.String,
|
|
881
|
+
compress: Schema.Boolean
|
|
882
|
+
}) {}
|
|
883
|
+
|
|
884
|
+
const processFiles = Effect.gen(function* () {
|
|
885
|
+
const fs = yield* FileSystem.FileSystem;
|
|
886
|
+
const path = yield* Path.Path;
|
|
887
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
888
|
+
|
|
889
|
+
// Load configuration
|
|
890
|
+
const configData = yield* fs.readFileString('config.json');
|
|
891
|
+
const config = yield* Schema.decodeUnknownEffect(
|
|
892
|
+
Schema.fromJsonString(FileProcessorConfig)
|
|
893
|
+
)(configData);
|
|
894
|
+
|
|
895
|
+
// Ensure output directory exists
|
|
896
|
+
yield* fs.makeDirectory(config.outputDir, { recursive: true });
|
|
897
|
+
|
|
898
|
+
// Read input files
|
|
899
|
+
const files = yield* fs.readDirectory(config.inputDir);
|
|
900
|
+
|
|
901
|
+
yield* Console.log(`Processing ${files.length} files...`);
|
|
902
|
+
|
|
903
|
+
// Process each file
|
|
904
|
+
yield* Effect.forEach(
|
|
905
|
+
files,
|
|
906
|
+
(file) =>
|
|
907
|
+
Effect.gen(function* () {
|
|
908
|
+
const inputPath = path.join(config.inputDir, file);
|
|
909
|
+
const outputPath = path.join(config.outputDir, file);
|
|
910
|
+
|
|
911
|
+
// Copy file
|
|
912
|
+
yield* fs.copy(inputPath, outputPath);
|
|
913
|
+
|
|
914
|
+
// Optionally compress
|
|
915
|
+
if (config.compress) {
|
|
916
|
+
yield* spawner.string(
|
|
917
|
+
ChildProcess.make('gzip', [outputPath])
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
yield* Console.log(`Processed: ${file}`);
|
|
922
|
+
}),
|
|
923
|
+
{ concurrency: 4 }
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
yield* Console.log('All files processed!');
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Run on Node.js
|
|
930
|
+
processFiles.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
931
|
+
|
|
932
|
+
// Or run on Bun - same code!
|
|
933
|
+
processFiles.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
## Testing with Platform Abstractions
|
|
937
|
+
|
|
938
|
+
One major benefit of platform abstractions is testability:
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
import { Effect, FileSystem, Layer } from 'effect';
|
|
942
|
+
|
|
943
|
+
declare const myFileProcessor: Effect.Effect<
|
|
944
|
+
void,
|
|
945
|
+
never,
|
|
946
|
+
FileSystem.FileSystem
|
|
947
|
+
>;
|
|
948
|
+
|
|
949
|
+
// Create mock FileSystem using makeNoop for testing
|
|
950
|
+
const TestFileSystem = Layer.succeed(
|
|
951
|
+
FileSystem.FileSystem,
|
|
952
|
+
FileSystem.makeNoop({
|
|
953
|
+
readFile: (path) => {
|
|
954
|
+
if (path === 'config.json') {
|
|
955
|
+
const data = JSON.stringify({ key: 'value' });
|
|
956
|
+
return Effect.succeed(new TextEncoder().encode(data));
|
|
957
|
+
}
|
|
958
|
+
return Effect.fail(new Error('File not found'));
|
|
959
|
+
},
|
|
960
|
+
exists: (path) => Effect.succeed(true)
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// Test your code
|
|
965
|
+
const testProgram = myFileProcessor.pipe(Effect.provide(TestFileSystem));
|
|
966
|
+
|
|
967
|
+
Effect.runPromise(testProgram);
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
## Quality Checklist
|
|
971
|
+
|
|
972
|
+
Before completing code that uses platform operations:
|
|
973
|
+
|
|
974
|
+
- [ ] All file I/O uses `FileSystem.FileSystem` service
|
|
975
|
+
- [ ] All path operations use `Path.Path` service
|
|
976
|
+
- [ ] Process spawning uses `ChildProcess` + `ChildProcessSpawner`
|
|
977
|
+
- [ ] Console output uses `Console.log` or `Effect.log` (not `console.log`)
|
|
978
|
+
- [ ] CLI arguments parsed with `effect/unstable/cli` (not `process.argv`)
|
|
979
|
+
- [ ] HTTP requests use `HttpClient.HttpClient` (not `fetch`/`axios`)
|
|
980
|
+
- [ ] Any raw `fetch` is isolated in a named low-level platform adapter with documented justification
|
|
981
|
+
- [ ] HTTP status is classified before success-body schema decoding
|
|
982
|
+
- [ ] Provider evidence is bounded and redacted; retry exhaustion remains a typed, observable failure
|
|
983
|
+
- [ ] Provider/network calls execute outside database transactions
|
|
984
|
+
- [ ] Automatic retries apply only to operations proven idempotent
|
|
985
|
+
- [ ] Adapter `layer` keeps `HttpClient.HttpClient` visible; optional `defaultLayer` owns transport wiring
|
|
986
|
+
- [ ] Cryptographic operations use `Crypto.Crypto` (not direct platform crypto APIs)
|
|
987
|
+
- [ ] Platform services accessed through Effect type system
|
|
988
|
+
- [ ] Appropriate platform/service layer provided (HTTP, sockets, workers, Redis, and other specialized integrations need service-specific layers, not just `NodeServices.layer` / `BunServices.layer`)
|
|
989
|
+
- [ ] No direct imports from `fs`, `path`, `child_process`, `http`, etc.
|
|
990
|
+
- [ ] No Bun-specific APIs (`Bun.file`, `Bun.spawn`, etc.)
|
|
991
|
+
- [ ] No browser-specific APIs without platform abstraction
|
|
992
|
+
- [ ] Code is testable with mock platform services
|
|
993
|
+
|
|
994
|
+
## Common Mistakes to Avoid
|
|
995
|
+
|
|
996
|
+
### 1. Mixing Platform APIs
|
|
997
|
+
|
|
998
|
+
```typescript
|
|
999
|
+
import { Effect, FileSystem } from 'effect';
|
|
1000
|
+
import fs from 'fs';
|
|
1001
|
+
|
|
1002
|
+
// ❌ WRONG - Mixing Effect Platform with direct APIs
|
|
1003
|
+
const bad = Effect.gen(function* () {
|
|
1004
|
+
const filesystem = yield* FileSystem.FileSystem;
|
|
1005
|
+
const content1 = yield* filesystem.readFileString('file1.txt');
|
|
1006
|
+
const content2 = fs.readFileSync('file2.txt', 'utf-8'); // Don't mix!
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// ✅ CORRECT - Use platform abstractions consistently
|
|
1010
|
+
const good = Effect.gen(function* () {
|
|
1011
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1012
|
+
const content1 = yield* fs.readFileString('file1.txt');
|
|
1013
|
+
const content2 = yield* fs.readFileString('file2.txt');
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### 2. Forgetting Platform Layer
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
1021
|
+
import { Effect, FileSystem } from 'effect';
|
|
1022
|
+
|
|
1023
|
+
// ❌ WRONG - No platform layer provided
|
|
1024
|
+
const program = Effect.gen(function* () {
|
|
1025
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1026
|
+
return yield* fs.readFileString('file.txt');
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
Effect.runPromise(program); // Runtime error!
|
|
1030
|
+
|
|
1031
|
+
// ✅ CORRECT - Provide platform layer
|
|
1032
|
+
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### 3. Using console.log
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
import { Console, Effect } from 'effect';
|
|
1039
|
+
|
|
1040
|
+
declare const someOperation: () => Effect.Effect<string>;
|
|
1041
|
+
|
|
1042
|
+
// ❌ WRONG - Direct console usage
|
|
1043
|
+
const badProgram = Effect.gen(function* () {
|
|
1044
|
+
console.log('Starting...');
|
|
1045
|
+
const result = yield* someOperation();
|
|
1046
|
+
console.log('Done!');
|
|
1047
|
+
return result;
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// ✅ CORRECT - Use Console or Effect.log
|
|
1051
|
+
const goodProgram = Effect.gen(function* () {
|
|
1052
|
+
yield* Console.log('Starting...');
|
|
1053
|
+
const result = yield* someOperation();
|
|
1054
|
+
yield* Console.log('Done!');
|
|
1055
|
+
return result;
|
|
1056
|
+
});
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## Migration Guide
|
|
1060
|
+
|
|
1061
|
+
### From Node.js fs to FileSystem
|
|
1062
|
+
|
|
1063
|
+
```typescript
|
|
1064
|
+
import { Effect, FileSystem } from 'effect';
|
|
1065
|
+
import fs from 'fs/promises';
|
|
1066
|
+
|
|
1067
|
+
// Before (Node.js)
|
|
1068
|
+
declare const fs: {
|
|
1069
|
+
readFile: (path: string, encoding: string) => Promise<string>;
|
|
1070
|
+
writeFile: (path: string, data: string) => Promise<void>;
|
|
1071
|
+
existsSync: (path: string) => boolean;
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
const data = await fs.readFile('file.txt', 'utf-8');
|
|
1075
|
+
await fs.writeFile('output.txt', data);
|
|
1076
|
+
const exists = fs.existsSync('config.json');
|
|
1077
|
+
|
|
1078
|
+
// After (Effect Platform)
|
|
1079
|
+
const program = Effect.gen(function* () {
|
|
1080
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1081
|
+
|
|
1082
|
+
const data = yield* fs.readFileString('file.txt');
|
|
1083
|
+
yield* fs.writeFileString('output.txt', data);
|
|
1084
|
+
const exists = yield* fs.exists('config.json');
|
|
1085
|
+
});
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### From fetch to HttpClient
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
import { Effect, Schema } from 'effect';
|
|
1092
|
+
import {
|
|
1093
|
+
HttpClient,
|
|
1094
|
+
HttpClientRequest,
|
|
1095
|
+
HttpClientResponse
|
|
1096
|
+
} from 'effect/unstable/http';
|
|
1097
|
+
|
|
1098
|
+
// Before (fetch)
|
|
1099
|
+
declare const fetch: (
|
|
1100
|
+
url: string,
|
|
1101
|
+
options: {
|
|
1102
|
+
method: string;
|
|
1103
|
+
headers: Record<string, string>;
|
|
1104
|
+
body: string;
|
|
1105
|
+
}
|
|
1106
|
+
) => Promise<{ json: () => Promise<unknown> }>;
|
|
1107
|
+
|
|
1108
|
+
const response = await fetch('https://api.example.com/data', {
|
|
1109
|
+
method: 'POST',
|
|
1110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1111
|
+
body: JSON.stringify({ key: 'value' })
|
|
1112
|
+
});
|
|
1113
|
+
const data = await response.json();
|
|
1114
|
+
|
|
1115
|
+
// After (Effect HttpClient)
|
|
1116
|
+
class CreateData extends Schema.Class<CreateData>('CreateData')({
|
|
1117
|
+
key: Schema.String
|
|
1118
|
+
}) {}
|
|
1119
|
+
|
|
1120
|
+
class CreatedData extends Schema.Class<CreatedData>('CreatedData')({
|
|
1121
|
+
id: Schema.String,
|
|
1122
|
+
key: Schema.String
|
|
1123
|
+
}) {}
|
|
1124
|
+
|
|
1125
|
+
const program = Effect.gen(function* () {
|
|
1126
|
+
const client = yield* HttpClient.HttpClient;
|
|
1127
|
+
|
|
1128
|
+
return yield* HttpClientRequest.post('https://api.example.com/data').pipe(
|
|
1129
|
+
HttpClientRequest.schemaBodyJson(CreateData)(new CreateData({ key: 'value' })),
|
|
1130
|
+
Effect.flatMap(client.execute),
|
|
1131
|
+
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
1132
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(CreatedData))
|
|
1133
|
+
);
|
|
1134
|
+
});
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
This POST is intentionally not retried. Add retry only if the provider offers a documented idempotency guarantee and the request supplies the required idempotency key.
|
|
1138
|
+
|
|
1139
|
+
### From child_process to ChildProcess
|
|
1140
|
+
|
|
1141
|
+
```typescript
|
|
1142
|
+
import { Effect } from 'effect';
|
|
1143
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
|
|
1144
|
+
import { exec } from 'child_process';
|
|
1145
|
+
import { promisify } from 'util';
|
|
1146
|
+
|
|
1147
|
+
// Before (child_process)
|
|
1148
|
+
declare const exec: (
|
|
1149
|
+
cmd: string,
|
|
1150
|
+
callback: (error: Error | null, result: { stdout: string }) => void
|
|
1151
|
+
) => void;
|
|
1152
|
+
declare const promisify: <T>(fn: T) => (...args: any[]) => Promise<any>;
|
|
1153
|
+
|
|
1154
|
+
const execAsync = promisify(exec);
|
|
1155
|
+
const { stdout } = await execAsync('git status');
|
|
1156
|
+
|
|
1157
|
+
// After (Effect ChildProcess)
|
|
1158
|
+
const program = Effect.gen(function* () {
|
|
1159
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
1160
|
+
const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));
|
|
1161
|
+
return stdout;
|
|
1162
|
+
});
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
## Summary
|
|
1166
|
+
|
|
1167
|
+
Effect provides a complete abstraction layer over platform-specific APIs, enabling you to:
|
|
1168
|
+
|
|
1169
|
+
1. **Write once, run anywhere** - Same code works on Node.js and Bun
|
|
1170
|
+
2. **Type-safe operations** - All errors tracked in Effect type signatures
|
|
1171
|
+
3. **Resource safety** - Automatic cleanup with Scope
|
|
1172
|
+
4. **Easy testing** - Mock services without touching the filesystem
|
|
1173
|
+
5. **Full Effect integration** - Compose with services, layers, and error handling
|
|
1174
|
+
|
|
1175
|
+
Always prefer Effect platform abstractions over direct platform APIs for maximum portability, safety, and testability.
|