opencode-effect-enforcer 0.2.8 → 0.4.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/README.md +6 -5
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +8 -6
- package/guidance/progressive-disclosure-guidance.md +15 -7
- package/package.json +6 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +1 -1
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +3 -3
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-decision-model/SKILL.md +301 -0
- package/skills/effect-ai-decision-model/openrouter.md +70 -0
- package/skills/effect-ai-language-model/SKILL.md +53 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +53 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
|
@@ -8,14 +8,14 @@ description: Use Effect platform abstractions for cross-platform file I/O, proce
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
10
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
-
|
|
11
|
+
This guide targets **Effect 4.0.0**. Inspect the `effect@4.0.0` tag when the checkout is ahead. Keep `effect` and every `@effect/*` package on the same version. APIs explicitly tagged `@stability unstable` can change in minor releases, including third-party Redis/client options.
|
|
12
12
|
|
|
13
13
|
Reference this for:
|
|
14
14
|
|
|
15
15
|
- FileSystem source: `packages/effect/src/FileSystem.ts`
|
|
16
16
|
- Path source: `packages/effect/src/Path.ts`
|
|
17
17
|
- Crypto source: `packages/effect/src/Crypto.ts`
|
|
18
|
-
- Socket source: `packages/effect/src/
|
|
18
|
+
- Socket source: `packages/effect/src/socket/`
|
|
19
19
|
- Platform layers: `packages/platform/node/`, `packages/platform/bun/`, and `packages/platform/browser/`
|
|
20
20
|
- Migration guide: `MIGRATION.md`
|
|
21
21
|
- Effect source: `packages/effect/src/`
|
|
@@ -83,6 +83,7 @@ const program = Effect.gen(function* () {
|
|
|
83
83
|
|
|
84
84
|
Easy to mock and stub services:
|
|
85
85
|
|
|
86
|
+
<!-- typecheck -->
|
|
86
87
|
```typescript
|
|
87
88
|
import { Effect, FileSystem, Layer } from 'effect';
|
|
88
89
|
|
|
@@ -90,7 +91,7 @@ declare const myProgram: Effect.Effect<void, never, FileSystem.FileSystem>;
|
|
|
90
91
|
|
|
91
92
|
const TestFileSystem = Layer.succeed(
|
|
92
93
|
FileSystem.FileSystem,
|
|
93
|
-
FileSystem.
|
|
94
|
+
FileSystem.makeNoop({
|
|
94
95
|
readFile: () => Effect.succeed(new Uint8Array())
|
|
95
96
|
})
|
|
96
97
|
);
|
|
@@ -102,11 +103,12 @@ const test = myProgram.pipe(Effect.provide(TestFileSystem));
|
|
|
102
103
|
|
|
103
104
|
Integrates naturally with Effect's service system:
|
|
104
105
|
|
|
106
|
+
<!-- typecheck -->
|
|
105
107
|
```typescript
|
|
106
|
-
import { Effect, FileSystem, Layer, Path, Context } from 'effect';
|
|
108
|
+
import { Effect, FileSystem, Layer, Path, PlatformError, Context } from 'effect';
|
|
107
109
|
|
|
108
110
|
interface ConfigService {
|
|
109
|
-
readonly load: (name: string) => Effect.Effect<string>;
|
|
111
|
+
readonly load: (name: string) => Effect.Effect<string, PlatformError.PlatformError>;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const ConfigService = Context.Service<ConfigService>('ConfigService');
|
|
@@ -118,11 +120,10 @@ const ConfigServiceLive = Layer.effect(
|
|
|
118
120
|
const path = yield* Path.Path;
|
|
119
121
|
|
|
120
122
|
return {
|
|
121
|
-
load: (name: string)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
})
|
|
123
|
+
load: Effect.fn('Config.load')(function* (name: string) {
|
|
124
|
+
const configPath = path.join('configs', name);
|
|
125
|
+
return yield* fs.readFileString(configPath);
|
|
126
|
+
})
|
|
126
127
|
};
|
|
127
128
|
})
|
|
128
129
|
);
|
|
@@ -155,21 +156,22 @@ const content = await file.text();
|
|
|
155
156
|
|
|
156
157
|
**Correct Pattern - FileSystem Service:**
|
|
157
158
|
|
|
159
|
+
<!-- typecheck -->
|
|
158
160
|
```typescript
|
|
159
161
|
import { Effect, FileSystem } from 'effect';
|
|
160
162
|
|
|
161
163
|
// ✅ CORRECT - Cross-platform, type-safe, testable
|
|
162
|
-
const readFile = (path: string)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
164
|
+
const readFile = Effect.fn('File.read')(function* (path: string) {
|
|
165
|
+
const fs = yield* FileSystem.FileSystem;
|
|
166
|
+
return yield* fs.readFileString(path);
|
|
167
|
+
});
|
|
167
168
|
|
|
168
169
|
// Effect<string, PlatformError, FileSystem>
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
**Common Operations:**
|
|
172
173
|
|
|
174
|
+
<!-- typecheck -->
|
|
173
175
|
```typescript
|
|
174
176
|
import { Effect, FileSystem } from 'effect';
|
|
175
177
|
|
|
@@ -210,6 +212,7 @@ const fileOperations = Effect.gen(function* () {
|
|
|
210
212
|
|
|
211
213
|
**Streaming Files:**
|
|
212
214
|
|
|
215
|
+
<!-- typecheck -->
|
|
213
216
|
```typescript
|
|
214
217
|
import { Effect, FileSystem, Stream } from 'effect';
|
|
215
218
|
|
|
@@ -224,7 +227,7 @@ const processLargeFile = Effect.gen(function* () {
|
|
|
224
227
|
|
|
225
228
|
// Process stream
|
|
226
229
|
yield* stream.pipe(
|
|
227
|
-
Stream.
|
|
230
|
+
Stream.tap(processChunk),
|
|
228
231
|
Stream.run(fs.sink('output.txt'))
|
|
229
232
|
);
|
|
230
233
|
});
|
|
@@ -252,6 +255,7 @@ const joined = path.join('src', 'components', 'Button.tsx');
|
|
|
252
255
|
|
|
253
256
|
**Correct Pattern - Path Service:**
|
|
254
257
|
|
|
258
|
+
<!-- typecheck -->
|
|
255
259
|
```typescript
|
|
256
260
|
import { Effect, Path } from 'effect';
|
|
257
261
|
|
|
@@ -281,6 +285,7 @@ const buildPath = (filename: string) =>
|
|
|
281
285
|
|
|
282
286
|
**Path Operations:**
|
|
283
287
|
|
|
288
|
+
<!-- typecheck -->
|
|
284
289
|
```typescript
|
|
285
290
|
import { Effect, Path } from 'effect';
|
|
286
291
|
|
|
@@ -315,7 +320,7 @@ In Effect v4, `Migrator.fromFileSystem` requires both `FileSystem.FileSystem` an
|
|
|
315
320
|
|
|
316
321
|
### ChildProcess - Process Execution
|
|
317
322
|
|
|
318
|
-
|
|
323
|
+
`ChildProcess` builds command values; the `ChildProcessSpawner` service executes them with scoped resource ownership. Output helpers do not reject nonzero exit codes: use a handle and check `exitCode` when status determines success. Drain combined output with `handle.all`, or consume stdout/stderr concurrently, before leaving the scope.
|
|
319
324
|
|
|
320
325
|
**Anti-Pattern - Direct child_process:**
|
|
321
326
|
|
|
@@ -341,8 +346,9 @@ const output = await new Response(proc.stdout).text();
|
|
|
341
346
|
|
|
342
347
|
**Correct Pattern - ChildProcess + ChildProcessSpawner:**
|
|
343
348
|
|
|
349
|
+
<!-- typecheck -->
|
|
344
350
|
```typescript
|
|
345
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
351
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
346
352
|
import { Effect, Stream } from 'effect';
|
|
347
353
|
|
|
348
354
|
// ✅ CORRECT - Cross-platform command execution
|
|
@@ -358,8 +364,9 @@ const runCommand = Effect.gen(function* () {
|
|
|
358
364
|
|
|
359
365
|
**Advanced ChildProcess Usage:**
|
|
360
366
|
|
|
367
|
+
<!-- typecheck -->
|
|
361
368
|
```typescript
|
|
362
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
369
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
363
370
|
import { Console, Effect, Stream } from 'effect';
|
|
364
371
|
|
|
365
372
|
const commandExamples = Effect.gen(function* () {
|
|
@@ -398,7 +405,7 @@ const commandExamples = Effect.gen(function* () {
|
|
|
398
405
|
const exitCode = yield* handle.exitCode;
|
|
399
406
|
|
|
400
407
|
return exitCode;
|
|
401
|
-
});
|
|
408
|
+
}).pipe(Effect.scoped);
|
|
402
409
|
```
|
|
403
410
|
|
|
404
411
|
### Terminal - Terminal I/O
|
|
@@ -428,6 +435,7 @@ const input = prompt('Enter name:');
|
|
|
428
435
|
|
|
429
436
|
**Correct Pattern - Terminal Service:**
|
|
430
437
|
|
|
438
|
+
<!-- typecheck -->
|
|
431
439
|
```typescript
|
|
432
440
|
import { Effect, Terminal } from 'effect';
|
|
433
441
|
|
|
@@ -451,6 +459,7 @@ const interactiveProgram = Effect.gen(function* () {
|
|
|
451
459
|
|
|
452
460
|
**For Simple Logging - Use Console or Effect.log:**
|
|
453
461
|
|
|
462
|
+
<!-- typecheck -->
|
|
454
463
|
```typescript
|
|
455
464
|
import { Console, Effect } from 'effect';
|
|
456
465
|
|
|
@@ -480,6 +489,7 @@ const structuredLog = Effect.gen(function* () {
|
|
|
480
489
|
|
|
481
490
|
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
491
|
|
|
492
|
+
<!-- typecheck -->
|
|
483
493
|
```typescript
|
|
484
494
|
import { Crypto, Effect } from 'effect';
|
|
485
495
|
|
|
@@ -522,8 +532,9 @@ const result = await axios.get('https://api.example.com/data');
|
|
|
522
532
|
|
|
523
533
|
**Correct Pattern - HttpClient Service:**
|
|
524
534
|
|
|
535
|
+
<!-- typecheck -->
|
|
525
536
|
```typescript
|
|
526
|
-
import { HttpClient, HttpClientResponse } from 'effect/
|
|
537
|
+
import { HttpClient, HttpClientResponse } from 'effect/http';
|
|
527
538
|
import { Effect, Schema } from 'effect';
|
|
528
539
|
|
|
529
540
|
// ✅ CORRECT - Integrated with Effect type system
|
|
@@ -545,12 +556,13 @@ Name the adapter service and its effects after the upstream operation. The adapt
|
|
|
545
556
|
|
|
546
557
|
**Advanced HTTP Operations:**
|
|
547
558
|
|
|
559
|
+
<!-- typecheck -->
|
|
548
560
|
```typescript
|
|
549
561
|
import {
|
|
550
562
|
HttpClient,
|
|
551
563
|
HttpClientRequest,
|
|
552
564
|
HttpClientResponse
|
|
553
|
-
} from 'effect/
|
|
565
|
+
} from 'effect/http';
|
|
554
566
|
import { Effect, Schema, Schedule } from 'effect';
|
|
555
567
|
|
|
556
568
|
class User extends Schema.Class<User>('User')({
|
|
@@ -597,20 +609,9 @@ const httpExamples = Effect.gen(function* () {
|
|
|
597
609
|
// Error handling — all HttpClient errors are "HttpClientError" with a reason field
|
|
598
610
|
const safeRequest = client.get('https://api.example.com/data').pipe(
|
|
599
611
|
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
600
|
-
Effect.
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
})
|
|
612
|
+
Effect.tapError((error) => Effect.logWarning('Provider request failed').pipe(
|
|
613
|
+
Effect.annotateLogs({ reason: error.reason._tag })
|
|
614
|
+
))
|
|
614
615
|
);
|
|
615
616
|
|
|
616
617
|
// Retries with backoff: GET is idempotent and attempts are bounded.
|
|
@@ -618,7 +619,9 @@ const httpExamples = Effect.gen(function* () {
|
|
|
618
619
|
Effect.flatMap(HttpClientResponse.filterStatusOk),
|
|
619
620
|
Effect.retry({
|
|
620
621
|
times: 3,
|
|
621
|
-
schedule: Schedule.exponential('100 millis')
|
|
622
|
+
schedule: Schedule.exponential('100 millis'),
|
|
623
|
+
while: (error) => error.reason._tag === 'TransportError' ||
|
|
624
|
+
(error.reason._tag === 'StatusCodeError' && error.reason.response.status >= 500)
|
|
622
625
|
}),
|
|
623
626
|
Effect.tapError((error) =>
|
|
624
627
|
Effect.logError('Provider read exhausted retries').pipe(
|
|
@@ -665,8 +668,9 @@ const value2 = fs.readFileSync('.cache/key', 'utf-8');
|
|
|
665
668
|
|
|
666
669
|
**Correct Pattern - KeyValueStore Service:**
|
|
667
670
|
|
|
671
|
+
<!-- typecheck -->
|
|
668
672
|
```typescript
|
|
669
|
-
import { KeyValueStore } from 'effect/
|
|
673
|
+
import { KeyValueStore } from 'effect/persistence';
|
|
670
674
|
import { Effect, Schema } from 'effect';
|
|
671
675
|
|
|
672
676
|
// ✅ CORRECT - Works on all platforms
|
|
@@ -697,8 +701,9 @@ const cacheData = Effect.gen(function* () {
|
|
|
697
701
|
|
|
698
702
|
**Schema-Based Store:**
|
|
699
703
|
|
|
704
|
+
<!-- typecheck -->
|
|
700
705
|
```typescript
|
|
701
|
-
import { KeyValueStore } from 'effect/
|
|
706
|
+
import { KeyValueStore } from 'effect/persistence';
|
|
702
707
|
import { Effect, Schema } from 'effect';
|
|
703
708
|
|
|
704
709
|
class User extends Schema.Class<User>('User')({
|
|
@@ -727,12 +732,14 @@ const typedStore = Effect.gen(function* () {
|
|
|
727
732
|
|
|
728
733
|
### Redis - Commands and Scoped Subscriptions
|
|
729
734
|
|
|
730
|
-
The portable `Redis.Redis` service in `effect/
|
|
735
|
+
The portable `Redis.Redis` service in `effect/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`.
|
|
736
|
+
|
|
737
|
+
The platform Redis APIs expose third-party options and are marked `@stability unstable`; inspect the installed package's contracts before minor-version upgrades.
|
|
731
738
|
|
|
732
739
|
```typescript
|
|
733
740
|
import { NodeRedis } from '@effect/platform-node';
|
|
734
741
|
import { Effect, Queue } from 'effect';
|
|
735
|
-
import { Redis } from 'effect/
|
|
742
|
+
import { Redis } from 'effect/persistence';
|
|
736
743
|
|
|
737
744
|
const RedisLayer = NodeRedis.layer({
|
|
738
745
|
database: 1,
|
|
@@ -748,9 +755,9 @@ const receiveOne = Effect.gen(function* () {
|
|
|
748
755
|
|
|
749
756
|
`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
757
|
|
|
751
|
-
### CLI Arguments - effect/
|
|
758
|
+
### CLI Arguments - effect/cli
|
|
752
759
|
|
|
753
|
-
For CLI applications, use `effect/
|
|
760
|
+
For CLI applications, use `effect/cli` instead of direct `process.argv`.
|
|
754
761
|
|
|
755
762
|
**Anti-Pattern - Direct process.argv:**
|
|
756
763
|
|
|
@@ -770,10 +777,10 @@ declare const yargs: (args: string[]) => { argv: Record<string, unknown> };
|
|
|
770
777
|
const argv = yargs(process.argv.slice(2)).argv;
|
|
771
778
|
```
|
|
772
779
|
|
|
773
|
-
**Correct Pattern - effect/
|
|
780
|
+
**Correct Pattern - effect/cli:**
|
|
774
781
|
|
|
775
782
|
```typescript
|
|
776
|
-
import { Argument, Command as CliCommand, Flag } from 'effect/
|
|
783
|
+
import { Argument, Command as CliCommand, Flag } from 'effect/cli';
|
|
777
784
|
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
778
785
|
import { Console, Effect } from 'effect';
|
|
779
786
|
|
|
@@ -816,20 +823,20 @@ Complete reference table of platform abstractions:
|
|
|
816
823
|
| ------------------------- | -------------------------------------- | ---------------------------- | ----------------------------- |
|
|
817
824
|
| **File I/O** | `FileSystem.FileSystem` | `fs`, `Bun.file` | `effect` |
|
|
818
825
|
| **Path Operations** | `Path.Path` | `path`, string concat | `effect` |
|
|
819
|
-
| **Process Spawning** | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/
|
|
826
|
+
| **Process Spawning** | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/process` |
|
|
820
827
|
| **Terminal I/O** | `Terminal.Terminal` | `process.stdin/stdout` | `effect` |
|
|
821
828
|
| **Console Logging** | `Console.log` or `Effect.log` | `console.log` | `effect` |
|
|
822
829
|
| **Crypto** | `Crypto.Crypto` | `globalThis.crypto`, `crypto.randomUUID()` | `effect` |
|
|
823
|
-
| **HTTP Client** | `HttpClient.HttpClient` | `fetch`, `axios` | `effect/
|
|
824
|
-
| **HTTP Server** | `HttpServer.HttpServer` | `http.createServer` | `effect/
|
|
825
|
-
| **Sockets** | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/
|
|
826
|
-
| **Key-Value Store** | `KeyValueStore.KeyValueStore` | `localStorage`, manual files | `effect/
|
|
827
|
-
| **Redis** | `Redis.Redis` | direct Redis clients | `effect/
|
|
828
|
-
| **CLI Arguments** | `Argument` + `Flag` + `Command` | `process.argv`, `yargs` | `effect/
|
|
830
|
+
| **HTTP Client** | `HttpClient.HttpClient` | `fetch`, `axios` | `effect/http` |
|
|
831
|
+
| **HTTP Server** | `HttpServer.HttpServer` | `http.createServer` | `effect/http` |
|
|
832
|
+
| **Sockets** | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/socket` |
|
|
833
|
+
| **Key-Value Store** | `KeyValueStore.KeyValueStore` | `localStorage`, manual files | `effect/persistence` |
|
|
834
|
+
| **Redis** | `Redis.Redis` | direct Redis clients | `effect/persistence` |
|
|
835
|
+
| **CLI Arguments** | `Argument` + `Flag` + `Command` | `process.argv`, `yargs` | `effect/cli` |
|
|
829
836
|
| **Environment Variables** | `Config` from effect | `process.env` | `effect` |
|
|
830
837
|
| **Streams** | `Stream` | Node streams, ReadableStream | `effect` |
|
|
831
838
|
|
|
832
|
-
Socket services live in `effect/
|
|
839
|
+
Socket services live in `effect/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
840
|
|
|
834
841
|
## Setting Up Platform-Specific Layers
|
|
835
842
|
|
|
@@ -871,7 +878,7 @@ program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
|
|
|
871
878
|
import { NodeServices, NodeRuntime } from '@effect/platform-node';
|
|
872
879
|
import { BunServices, BunRuntime } from '@effect/platform-bun';
|
|
873
880
|
import { Console, Effect, FileSystem, Path, Schema } from 'effect';
|
|
874
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
881
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
875
882
|
|
|
876
883
|
class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
|
|
877
884
|
'FileProcessorConfig'
|
|
@@ -881,6 +888,11 @@ class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
|
|
|
881
888
|
compress: Schema.Boolean
|
|
882
889
|
}) {}
|
|
883
890
|
|
|
891
|
+
class CompressionFailed extends Schema.TaggedError<CompressionFailed>()('CompressionFailed', {
|
|
892
|
+
file: Schema.String,
|
|
893
|
+
exitCode: Schema.Number
|
|
894
|
+
}) {}
|
|
895
|
+
|
|
884
896
|
const processFiles = Effect.gen(function* () {
|
|
885
897
|
const fs = yield* FileSystem.FileSystem;
|
|
886
898
|
const path = yield* Path.Path;
|
|
@@ -913,9 +925,14 @@ const processFiles = Effect.gen(function* () {
|
|
|
913
925
|
|
|
914
926
|
// Optionally compress
|
|
915
927
|
if (config.compress) {
|
|
916
|
-
yield* spawner.
|
|
917
|
-
ChildProcess.make('gzip', [outputPath]
|
|
928
|
+
const exitCode = yield* spawner.exitCode(
|
|
929
|
+
ChildProcess.make('gzip', [outputPath], {
|
|
930
|
+
stdin: 'ignore', stdout: 'ignore', stderr: 'inherit'
|
|
931
|
+
})
|
|
918
932
|
);
|
|
933
|
+
if (exitCode !== 0) {
|
|
934
|
+
return yield* Effect.fail(new CompressionFailed({ file, exitCode }));
|
|
935
|
+
}
|
|
919
936
|
}
|
|
920
937
|
|
|
921
938
|
yield* Console.log(`Processed: ${file}`);
|
|
@@ -937,8 +954,9 @@ processFiles.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
|
|
|
937
954
|
|
|
938
955
|
One major benefit of platform abstractions is testability:
|
|
939
956
|
|
|
957
|
+
<!-- typecheck -->
|
|
940
958
|
```typescript
|
|
941
|
-
import { Effect, FileSystem, Layer } from 'effect';
|
|
959
|
+
import { Effect, FileSystem, Layer, PlatformError } from 'effect';
|
|
942
960
|
|
|
943
961
|
declare const myFileProcessor: Effect.Effect<
|
|
944
962
|
void,
|
|
@@ -950,14 +968,16 @@ declare const myFileProcessor: Effect.Effect<
|
|
|
950
968
|
const TestFileSystem = Layer.succeed(
|
|
951
969
|
FileSystem.FileSystem,
|
|
952
970
|
FileSystem.makeNoop({
|
|
953
|
-
|
|
971
|
+
readFileString: (path) => {
|
|
954
972
|
if (path === 'config.json') {
|
|
955
|
-
|
|
956
|
-
return Effect.succeed(new TextEncoder().encode(data));
|
|
973
|
+
return Effect.succeed('{"key":"value"}');
|
|
957
974
|
}
|
|
958
|
-
return Effect.fail(
|
|
975
|
+
return Effect.fail(PlatformError.systemError({
|
|
976
|
+
_tag: 'NotFound', module: 'FileSystem', method: 'readFileString',
|
|
977
|
+
pathOrDescriptor: path
|
|
978
|
+
}));
|
|
959
979
|
},
|
|
960
|
-
exists: (path) => Effect.succeed(
|
|
980
|
+
exists: (path) => Effect.succeed(path === 'config.json')
|
|
961
981
|
})
|
|
962
982
|
);
|
|
963
983
|
|
|
@@ -967,6 +987,8 @@ const testProgram = myFileProcessor.pipe(Effect.provide(TestFileSystem));
|
|
|
967
987
|
Effect.runPromise(testProgram);
|
|
968
988
|
```
|
|
969
989
|
|
|
990
|
+
`makeNoop` does not derive `readFileString` from an overridden `readFile`; override the methods your program calls. Most missing operations fail with NotFound, `exists` defaults to false, `remove` succeeds, and directory/temp creation defects unless overridden. `make` is for implementing the full set of core operations and deriving conveniences, not for partial test doubles.
|
|
991
|
+
|
|
970
992
|
## Quality Checklist
|
|
971
993
|
|
|
972
994
|
Before completing code that uses platform operations:
|
|
@@ -975,7 +997,7 @@ Before completing code that uses platform operations:
|
|
|
975
997
|
- [ ] All path operations use `Path.Path` service
|
|
976
998
|
- [ ] Process spawning uses `ChildProcess` + `ChildProcessSpawner`
|
|
977
999
|
- [ ] Console output uses `Console.log` or `Effect.log` (not `console.log`)
|
|
978
|
-
- [ ] CLI arguments parsed with `effect/
|
|
1000
|
+
- [ ] CLI arguments parsed with `effect/cli` (not `process.argv`)
|
|
979
1001
|
- [ ] HTTP requests use `HttpClient.HttpClient` (not `fetch`/`axios`)
|
|
980
1002
|
- [ ] Any raw `fetch` is isolated in a named low-level platform adapter with documented justification
|
|
981
1003
|
- [ ] HTTP status is classified before success-body schema decoding
|
|
@@ -1026,7 +1048,7 @@ const program = Effect.gen(function* () {
|
|
|
1026
1048
|
return yield* fs.readFileString('file.txt');
|
|
1027
1049
|
});
|
|
1028
1050
|
|
|
1029
|
-
Effect.runPromise(program); //
|
|
1051
|
+
Effect.runPromise(program); // Type error: FileSystem requirement is unsatisfied
|
|
1030
1052
|
|
|
1031
1053
|
// ✅ CORRECT - Provide platform layer
|
|
1032
1054
|
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
@@ -1093,7 +1115,7 @@ import {
|
|
|
1093
1115
|
HttpClient,
|
|
1094
1116
|
HttpClientRequest,
|
|
1095
1117
|
HttpClientResponse
|
|
1096
|
-
} from 'effect/
|
|
1118
|
+
} from 'effect/http';
|
|
1097
1119
|
|
|
1098
1120
|
// Before (fetch)
|
|
1099
1121
|
declare const fetch: (
|
|
@@ -1140,7 +1162,7 @@ This POST is intentionally not retried. Add retry only if the provider offers a
|
|
|
1140
1162
|
|
|
1141
1163
|
```typescript
|
|
1142
1164
|
import { Effect } from 'effect';
|
|
1143
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
1165
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
1144
1166
|
import { exec } from 'child_process';
|
|
1145
1167
|
import { promisify } from 'util';
|
|
1146
1168
|
|