opencode-effect-enforcer 0.2.5 → 0.2.8
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 +42 -140
- package/docs/effect-4.0.0-rc.116-changelog.md +2654 -0
- package/docs/effect-4.0.0-rc.116.md +102 -0
- package/guidance/effect-first-development.md +28 -311
- package/guidance/progressive-disclosure-guidance.md +5 -19
- package/package.json +2 -2
- package/patterns/avoid-direct-tag-checks.md +1 -1
- package/patterns/avoid-process-env.md +4 -4
- package/patterns/context-tag-extends.md +4 -4
- package/patterns/prefer-arr-sort.md +1 -1
- package/patterns/prefer-redacted-config.md +10 -10
- package/patterns/prefer-schema-class.md +1 -1
- package/patterns/require-effect-concurrency.md +1 -1
- package/skills/effect-ai-chat/SKILL.md +2 -2
- package/skills/effect-ai-language-model/SKILL.md +36 -4
- package/skills/effect-ai-prompt/SKILL.md +1 -1
- package/skills/effect-ai-provider/SKILL.md +23 -12
- package/skills/effect-ai-tool/SKILL.md +13 -0
- package/skills/effect-atom-rpc/SKILL.md +7 -1
- package/skills/effect-atom-state/SKILL.md +8 -2
- package/skills/effect-cache/SKILL.md +10 -1
- package/skills/effect-cli/SKILL.md +105 -94
- package/skills/effect-command-executor/SKILL.md +7 -1
- package/skills/effect-config/SKILL.md +67 -44
- package/skills/effect-domain-modeling/SKILL.md +3 -3
- package/skills/effect-error-handling/SKILL.md +2 -2
- package/skills/effect-fiber/SKILL.md +2 -2
- package/skills/effect-filesystem/SKILL.md +34 -4
- package/skills/effect-http-api/SKILL.md +17 -2
- package/skills/effect-http-client/SKILL.md +11 -2
- package/skills/effect-http-server/SKILL.md +28 -11
- package/skills/effect-layer-design/SKILL.md +6 -2
- package/skills/effect-mcp-server/SKILL.md +21 -4
- package/skills/effect-observability/SKILL.md +2 -2
- package/skills/effect-optics/SKILL.md +1 -1
- package/skills/effect-parallelization/SKILL.md +2 -2
- package/skills/effect-pattern-matching/SKILL.md +1 -1
- package/skills/effect-platform-abstraction/SKILL.md +3 -3
- package/skills/effect-rpc-api/SKILL.md +3 -3
- package/skills/effect-rpc-client/SKILL.md +14 -13
- package/skills/effect-rpc-cluster/SKILL.md +15 -12
- package/skills/effect-rpc-server/SKILL.md +11 -13
- package/skills/effect-scheduling/SKILL.md +7 -0
- package/skills/effect-schema-composition/SKILL.md +12 -4
- package/skills/effect-schema-v4/SKILL.md +75 -20
- package/skills/effect-scope/SKILL.md +4 -4
- package/skills/effect-socket/SKILL.md +161 -658
- package/skills/effect-sql/SKILL.md +50 -12
- package/skills/effect-stream/SKILL.md +19 -24
- package/skills/effect-testing/SKILL.md +35 -22
- package/skills/effect-workflow/SKILL.md +13 -2
- package/src/guidance.ts +0 -1
|
@@ -37,20 +37,20 @@ Never read `process.env` directly in Effect code. `Config` provides:
|
|
|
37
37
|
Each constructor reads a single value and decodes it. The optional `name` parameter sets the root path segment for lookup. Omit it when the config is part of a larger `Config.schema`.
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
Config.
|
|
41
|
-
Config.
|
|
42
|
-
Config.
|
|
43
|
-
Config.
|
|
44
|
-
Config.
|
|
45
|
-
Config.
|
|
46
|
-
Config.
|
|
47
|
-
Config.
|
|
48
|
-
Config.
|
|
49
|
-
Config.
|
|
50
|
-
Config.
|
|
51
|
-
Config.
|
|
52
|
-
Config.
|
|
53
|
-
Config.
|
|
40
|
+
Config.String('HOST'); // string
|
|
41
|
+
Config.NonEmptyString('HOST'); // string (rejects "")
|
|
42
|
+
Config.Number('RATE'); // number (includes NaN, Infinity)
|
|
43
|
+
Config.Finite('RATE'); // number (rejects NaN, Infinity)
|
|
44
|
+
Config.Int('PORT'); // number (integers only)
|
|
45
|
+
Config.Boolean('DEBUG'); // boolean (accepts true/false, yes/no, on/off, 1/0, y/n)
|
|
46
|
+
Config.Port('PORT'); // number (integer in 1–65535)
|
|
47
|
+
Config.URL('CALLBACK_URL'); // URL
|
|
48
|
+
Config.Date('EXPIRES_AT'); // Date (rejects invalid dates)
|
|
49
|
+
Config.Duration('TIMEOUT'); // Duration (parses "10 seconds", "500 millis", "Infinity", "-Infinity")
|
|
50
|
+
Config.LogLevel('LOG_LEVEL'); // string (All|Fatal|Error|Warn|Info|Debug|Trace|None)
|
|
51
|
+
Config.Redacted('API_KEY'); // Redacted<string> (hidden from logs and toString)
|
|
52
|
+
Config.Literal('production', 'ENV'); // literal type (accepts only the given literal)
|
|
53
|
+
Config.Literals(['development', 'production'], 'ENV'); // accepts one of several literals
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
## Config Combinators
|
|
@@ -60,7 +60,7 @@ Config.literals(['development', 'production'], 'ENV'); // accepts one of several
|
|
|
60
60
|
Only triggers when data is **missing**. Validation errors (wrong type, out of range) still propagate.
|
|
61
61
|
|
|
62
62
|
```ts
|
|
63
|
-
const port = Config.
|
|
63
|
+
const port = Config.Int('PORT').pipe(Config.withDefault(3000));
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
### `Config.option` — Optional Values
|
|
@@ -68,13 +68,13 @@ const port = Config.int('PORT').pipe(Config.withDefault(3000));
|
|
|
68
68
|
Returns `Option.some(value)` on success, `Option.none()` when data is missing.
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
|
-
const maybePort = Config.option(Config.
|
|
71
|
+
const maybePort = Config.option(Config.Int('PORT'));
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
### `Config.map` — Transform a Value
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
|
-
const upperHost = Config.
|
|
77
|
+
const upperHost = Config.String('HOST').pipe(
|
|
78
78
|
Config.map((s) => s.toUpperCase())
|
|
79
79
|
);
|
|
80
80
|
```
|
|
@@ -84,7 +84,7 @@ const upperHost = Config.string('HOST').pipe(
|
|
|
84
84
|
Unlike `withDefault`, this catches **all** `ConfigError`s:
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
|
-
const host = Config.
|
|
87
|
+
const host = Config.String('HOST').pipe(
|
|
88
88
|
Config.orElse(() => Config.succeed('localhost'))
|
|
89
89
|
);
|
|
90
90
|
```
|
|
@@ -96,13 +96,13 @@ Accepts a record or a tuple:
|
|
|
96
96
|
```ts
|
|
97
97
|
// As a record
|
|
98
98
|
const appConfig = Config.all({
|
|
99
|
-
host: Config.
|
|
100
|
-
port: Config.
|
|
101
|
-
debug: Config.
|
|
99
|
+
host: Config.String('host'),
|
|
100
|
+
port: Config.Int('port'),
|
|
101
|
+
debug: Config.Boolean('debug')
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
// As a tuple
|
|
105
|
-
const pair = Config.all([Config.
|
|
105
|
+
const pair = Config.all([Config.String('a'), Config.Int('b')]);
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
### `Config.nested` — Scope Under a Prefix
|
|
@@ -111,8 +111,8 @@ Prepends a path segment to every key the inner config reads. With environment va
|
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
const dbConfig = Config.all({
|
|
114
|
-
host: Config.
|
|
115
|
-
port: Config.
|
|
114
|
+
host: Config.String('host'),
|
|
115
|
+
port: Config.Int('port')
|
|
116
116
|
}).pipe(Config.nested('database'));
|
|
117
117
|
|
|
118
118
|
// Reads from env: database_host, database_port
|
|
@@ -146,17 +146,40 @@ const ServerConfig = Config.schema(
|
|
|
146
146
|
);
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
### Config
|
|
149
|
+
### Config constructors versus schemas
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
| `Config.Port` | `number` | Integer in 1–65535 |
|
|
156
|
-
| `Config.LogLevel` | `string` | One of the standard log level literals |
|
|
157
|
-
| `Config.Record(key, value)` | `Record<K, V>` | Also parses flat `"k1=v1,k2=v2"` strings |
|
|
151
|
+
PascalCase names on `Config` construct configs, not schemas. Combine
|
|
152
|
+
`Config.Boolean`, `Config.Port`, and `Config.LogLevel` with `Config.all`; use
|
|
153
|
+
Schema values inside `Config.schema`. The specialized implementation schemas
|
|
154
|
+
are internal. `Config.mapEffect` is the effectful mapping combinator.
|
|
158
155
|
|
|
159
|
-
|
|
156
|
+
<!-- typecheck -->
|
|
157
|
+
```ts
|
|
158
|
+
import { Config, ConfigProvider, Schema } from 'effect';
|
|
159
|
+
|
|
160
|
+
const settings = Config.all({
|
|
161
|
+
port: Config.Port('PORT'),
|
|
162
|
+
debug: Config.Boolean('DEBUG'),
|
|
163
|
+
exporters: Config.Array(Schema.String, 'EXPORTERS'),
|
|
164
|
+
labels: Config.Record(Schema.String, Schema.String, 'LABELS'),
|
|
165
|
+
limit: Config.ByteSize('LIMIT')
|
|
166
|
+
});
|
|
167
|
+
const parsed = settings.parse(ConfigProvider.fromUnknown({
|
|
168
|
+
PORT: '8080', DEBUG: 'yes', EXPORTERS: 'otlp,console',
|
|
169
|
+
LABELS: 'service.name=api', LIMIT: '64 KiB'
|
|
170
|
+
}));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`Config.Array(value, path?, options?)` and `Config.Record(key, value, path?, options?)`
|
|
174
|
+
also accept an options object without a path. They read structural values or
|
|
175
|
+
separated strings. Plain `Schema.Array` / `Schema.Record` in `Config.schema` load
|
|
176
|
+
structural children. Opaque encodings such as `Schema.Any`, `Schema.Unknown`, and
|
|
177
|
+
`Schema.Json` are rejected; use a concrete shape or
|
|
178
|
+
`Schema.fromJsonString(Schema.Json)` for scalar JSON.
|
|
179
|
+
|
|
180
|
+
Environment/bracket array indices must be unpadded decimal integers from 0 through
|
|
181
|
+
4294967294. Numeric-looking keys such as `01` remain object keys; use `[1]` for
|
|
182
|
+
array paths.
|
|
160
183
|
|
|
161
184
|
Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide semantic absence. A successful decoded `undefined` or an explicitly present empty structure remains a real value and is not replaced by a default.
|
|
162
185
|
|
|
@@ -166,8 +189,8 @@ Missing or unavailable representations are decoded as `undefined` before `Config
|
|
|
166
189
|
|
|
167
190
|
```ts
|
|
168
191
|
const program = Effect.gen(function* () {
|
|
169
|
-
const host = yield* Config.
|
|
170
|
-
const port = yield* Config.
|
|
192
|
+
const host = yield* Config.String('HOST');
|
|
193
|
+
const port = yield* Config.Int('PORT');
|
|
171
194
|
console.log(`${host}:${port}`);
|
|
172
195
|
});
|
|
173
196
|
```
|
|
@@ -175,7 +198,7 @@ const program = Effect.gen(function* () {
|
|
|
175
198
|
### 2. Call `.parse(provider)` directly — useful for testing
|
|
176
199
|
|
|
177
200
|
```ts
|
|
178
|
-
const host = Config.
|
|
201
|
+
const host = Config.String('HOST');
|
|
179
202
|
const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
|
|
180
203
|
const result = Effect.runSync(host.parse(provider));
|
|
181
204
|
// "localhost"
|
|
@@ -368,7 +391,7 @@ const TestLayer = ConfigProvider.layer(
|
|
|
368
391
|
);
|
|
369
392
|
|
|
370
393
|
const program = Effect.gen(function* () {
|
|
371
|
-
const port = yield* Config.
|
|
394
|
+
const port = yield* Config.Int('port');
|
|
372
395
|
return port;
|
|
373
396
|
});
|
|
374
397
|
|
|
@@ -416,7 +439,7 @@ const DefaultsLayer = ConfigProvider.layerAdd(
|
|
|
416
439
|
const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
|
|
417
440
|
|
|
418
441
|
const program = Effect.gen(function* () {
|
|
419
|
-
const host = yield* Config.
|
|
442
|
+
const host = yield* Config.String('HOST');
|
|
420
443
|
return host;
|
|
421
444
|
}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider));
|
|
422
445
|
```
|
|
@@ -430,8 +453,8 @@ import { Config, ConfigProvider, Effect } from 'effect';
|
|
|
430
453
|
|
|
431
454
|
// Pattern 1: .parse(provider) for direct testing
|
|
432
455
|
const config = Config.all({
|
|
433
|
-
host: Config.
|
|
434
|
-
port: Config.
|
|
456
|
+
host: Config.String('host'),
|
|
457
|
+
port: Config.Int('port')
|
|
435
458
|
});
|
|
436
459
|
|
|
437
460
|
const testProvider = ConfigProvider.fromUnknown({
|
|
@@ -451,7 +474,7 @@ const TestConfigLayer = ConfigProvider.layer(
|
|
|
451
474
|
);
|
|
452
475
|
|
|
453
476
|
const program = Effect.gen(function* () {
|
|
454
|
-
const host = yield* Config.
|
|
477
|
+
const host = yield* Config.String('host').pipe(Config.nested('server'));
|
|
455
478
|
return host;
|
|
456
479
|
});
|
|
457
480
|
|
|
@@ -466,7 +489,7 @@ Config operations fail with `ConfigError`, which wraps either:
|
|
|
466
489
|
- **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key)
|
|
467
490
|
|
|
468
491
|
```ts
|
|
469
|
-
const program = Config.
|
|
492
|
+
const program = Config.Int('PORT')
|
|
470
493
|
.parse(ConfigProvider.fromUnknown({ PORT: 'not-a-number' }))
|
|
471
494
|
.pipe(
|
|
472
495
|
Effect.tapError((error) =>
|
|
@@ -510,7 +533,7 @@ const DbConfig = Config.schema(
|
|
|
510
533
|
const AppConfig = Config.all({
|
|
511
534
|
server: ServerConfig,
|
|
512
535
|
db: DbConfig,
|
|
513
|
-
debug: Config.
|
|
536
|
+
debug: Config.Boolean('debug').pipe(Config.withDefault(false))
|
|
514
537
|
});
|
|
515
538
|
|
|
516
539
|
// In production — just yield it, reads from process.env
|
|
@@ -551,7 +574,7 @@ debug=true
|
|
|
551
574
|
const port = parseInt(process.env.PORT ?? '3000');
|
|
552
575
|
|
|
553
576
|
// GOOD
|
|
554
|
-
const port = Config.
|
|
577
|
+
const port = Config.Int('PORT').pipe(Config.withDefault(3000));
|
|
555
578
|
```
|
|
556
579
|
|
|
557
580
|
### NEVER validate config manually
|
|
@@ -13,7 +13,7 @@ and `effect-typeclass-design` for reusable predicate/order APIs.
|
|
|
13
13
|
|
|
14
14
|
## Source Reference
|
|
15
15
|
|
|
16
|
-
Baseline: **Effect 4.0.0-rc.
|
|
16
|
+
Baseline: **Effect 4.0.0-rc.116**. In the Effect source reference, consult
|
|
17
17
|
`packages/effect/SCHEMA.md` and `packages/effect/src/{Schema,Match,DateTime,Order}.ts`.
|
|
18
18
|
Verify the installed version and source tag before applying newer APIs.
|
|
19
19
|
|
|
@@ -159,7 +159,7 @@ message. Never claim the invariant is enforced solely because fields use
|
|
|
159
159
|
`matchOrElse`. Use it when plain internal object variants are intentional.
|
|
160
160
|
- `Data.TaggedEnum` is useful for trusted, non-schema types. It does not decode
|
|
161
161
|
unknown input; do not duplicate a schema model with a parallel Data union.
|
|
162
|
-
- Use `.match` for exhaustiveness. Use
|
|
162
|
+
- Use `.match` for exhaustiveness. Use `.matchOrElse(cases, fallback)` or
|
|
163
163
|
`.matchOrElse(value, cases, fallback)` when one fallback truthfully handles all
|
|
164
164
|
other cases. The fallback from `toTaggedUnion` is narrowed to unmatched
|
|
165
165
|
variants; direct `Schema.TaggedUnion.matchOrElse` types it as the full union.
|
|
@@ -228,7 +228,7 @@ see `effect-schema-composition` for transformation and recursion details.
|
|
|
228
228
|
- Use `Schema.toEquivalence` for model comparisons. `Eq.equals` provides general
|
|
229
229
|
structural equality in v4, but schema equivalence expresses model intent.
|
|
230
230
|
- Compose `Order.mapInput` / `Order.combine` and sort with `Arr.sort`. Finite
|
|
231
|
-
`Arr.groupBy` / `Iterable.groupBy`
|
|
231
|
+
`Arr.groupBy` / `Iterable.groupBy` preserve finite keys, with optional
|
|
232
232
|
properties: a particular group may not exist. Handle that absence explicitly.
|
|
233
233
|
- Use `DateTime` for instants, `Duration` for intervals, and `DateTime.now` for
|
|
234
234
|
effectful current time. Deterministic fixtures may use `DateTime.makeUnsafe`
|
|
@@ -84,7 +84,7 @@ Reference this for:
|
|
|
84
84
|
| `Schema.TaggedErrorClass` | `Schema.TaggedError` |
|
|
85
85
|
| `Schema.ErrorClass` | `Schema.Error` |
|
|
86
86
|
| `Schema.Error` (instance schema) | `Schema.ErrorInstance` |
|
|
87
|
-
| `Schema.ErrorReviver` | `
|
|
87
|
+
| `Schema.ErrorReviver` | `SchemaRepresentation.ErrorInstanceReviver` |
|
|
88
88
|
| `ParseError` | `Schema.SchemaError` |
|
|
89
89
|
|
|
90
90
|
## Core Error Handling Philosophy
|
|
@@ -592,7 +592,7 @@ const program2 = riskyOp().pipe(
|
|
|
592
592
|
);
|
|
593
593
|
```
|
|
594
594
|
|
|
595
|
-
> **Type preservation
|
|
595
|
+
> **Type preservation:** When you omit `orElse`, unhandled tags stay in the error channel — `catchTag(['NotFound'], ...)` on `Effect<string, NotFound | Forbidden | ServerError>` yields `Effect<string, Forbidden | ServerError>`. Supplying `orElse` handles the remaining variants, so the resulting error channel reflects what the fallback produces.
|
|
596
596
|
|
|
597
597
|
### catchTags - Multiple Error Types
|
|
598
598
|
|
|
@@ -555,7 +555,7 @@ This is exactly what the FiberHandle/Map/Set runtime helpers wrap for you — pr
|
|
|
555
555
|
|
|
556
556
|
### Keep-alive and runMain
|
|
557
557
|
|
|
558
|
-
|
|
558
|
+
There is **no per-fiber keep-alive in the core runtime**. A bare `Effect.runFork`/`Effect.runPromise` whose fiber is suspended on a pure Effect primitive (e.g. `Deferred.await`, `Effect.never`) does not by itself hold the Node.js process open.
|
|
559
559
|
|
|
560
560
|
`Runtime.makeRunMain`-based runners — `NodeRuntime.runMain` from `@effect/platform-node`, `BunRuntime.runMain`, etc. — install a long-interval timer that keeps the process alive until the main fiber completes, and additionally provide SIGINT/SIGTERM handling (interrupting the root fiber gracefully), exit-code mapping (interruption-only causes → 130), and error reporting. Always use `runMain` for long-lived program entry points:
|
|
561
561
|
|
|
@@ -724,7 +724,7 @@ const handoff = Effect.gen(function* () {
|
|
|
724
724
|
10. **Expecting `onlyIfMissing: true` to error when occupied** — it succeeds, returning a shared already-interrupted fiber while keeping the existing one. Check `Exit.hasInterrupts(yield* Fiber.await(fiber))` to detect the rejected start.
|
|
725
725
|
11. **Calling `run` on a closed collection** — `FiberHandle.run`/`FiberMap.run` interrupt the *calling* fiber; `FiberSet.run` and all `runtime()` runners return a pre-interrupted fiber instead. Neither throws.
|
|
726
726
|
12. **Assuming collection fibers are children of the caller** — they are root fibers created via `Effect.runForkWith` with the caller's context: they start immediately and survive the calling fiber; only the collection (scope close, replacement, remove/clear) interrupts them.
|
|
727
|
-
13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** —
|
|
727
|
+
13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** — a fiber suspended on `Deferred.await`/`Effect.never` won't hold the process open. Use `NodeRuntime.runMain` (built on `Runtime.makeRunMain`).
|
|
728
728
|
14. **Letting interruption leak through acquire/release** — wrap the whole sequence in `Effect.uninterruptibleMask` and `restore` only the use phase; pending interruption is delivered as soon as the region ends, so cleanup still runs exactly once.
|
|
729
729
|
15. **Leaving collection type parameters off** — `FiberHandle.make()` defaults to `<unknown, unknown>`, making `join` surface `unknown` errors. Always pass them: `FiberHandle.make<A, E>()`, `FiberMap.make<K, A, E>()`, `FiberSet.make<A, E>()`.
|
|
730
730
|
16. **Using v3 `FiberId` types** — v4 fiber ids are plain `number`s; `Cause.interruptors(cause)` and `Effect.onInterrupt` finalizers give you `ReadonlySet<number>`.
|
|
@@ -207,16 +207,46 @@ const useFileHandle = Effect.gen(function* () {
|
|
|
207
207
|
Effect.gen(function* () {
|
|
208
208
|
const file = yield* fs.open('data.txt', { flag: 'r' });
|
|
209
209
|
|
|
210
|
-
//
|
|
210
|
+
// Byte counts use ByteSize; seek positions are signed bigint inputs.
|
|
211
211
|
const buffer = new Uint8Array(1024);
|
|
212
212
|
const bytesRead = yield* file.read(buffer);
|
|
213
|
-
const offset = yield* file.seek(
|
|
213
|
+
const offset = yield* file.seek(0n, 'start');
|
|
214
214
|
})
|
|
215
215
|
);
|
|
216
216
|
});
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
`file.seek(offset, from)` accepts
|
|
219
|
+
`file.seek(offset, from)` accepts `bigint`, supports `from: 'start' | 'current'`,
|
|
220
|
+
and returns `Effect<bigint, PlatformError>`. A negative resulting position fails
|
|
221
|
+
without changing the cursor. Byte counts and `File.Info.size` use
|
|
222
|
+
`ByteSize.ByteSize`; size inputs use `ByteSize.Input`. Keep large offsets/counts
|
|
223
|
+
exact, and handle `Option.none()` for optional stat metadata outside the safe
|
|
224
|
+
integer range. Use scoped handle operations (`read`, `readAlloc`, `write`,
|
|
225
|
+
`writeAll`, `seek`, `stat`, `sync`, `truncate`) rather than raw descriptors.
|
|
226
|
+
|
|
227
|
+
### Byte sizes and exact offsets
|
|
228
|
+
|
|
229
|
+
<!-- typecheck -->
|
|
230
|
+
```ts
|
|
231
|
+
import { ByteSize, Effect, FileSystem } from 'effect';
|
|
232
|
+
|
|
233
|
+
const inspect = Effect.gen(function* () {
|
|
234
|
+
const fs = yield* FileSystem.FileSystem;
|
|
235
|
+
const file = yield* fs.open('data.bin');
|
|
236
|
+
const position = yield* file.seek(0n, 'start');
|
|
237
|
+
const chunk = yield* file.readAlloc(64 * 1024);
|
|
238
|
+
return { position, chunk };
|
|
239
|
+
}).pipe(Effect.scoped);
|
|
240
|
+
const bodyLimit = ByteSize.mebibytes(1);
|
|
241
|
+
const parsedLimit = ByteSize.fromString('1.5 MiB');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`ByteSize.Input` string literals are canonical non-negative integers with recognized
|
|
245
|
+
units. Parse external strings or fractional quantities with `ByteSize.fromString`
|
|
246
|
+
(or the explicitly throwing `fromStringUnsafe` at a controlled boundary).
|
|
247
|
+
Do not convert large sizes/offsets to number and silently lose precision.
|
|
248
|
+
Buffer allocation lengths (`readAlloc`) and per-buffer read/write counts are
|
|
249
|
+
numbers. File stat sizes and streaming byte limits use ByteSize; seek uses bigint.
|
|
220
250
|
|
|
221
251
|
## Directory Operations
|
|
222
252
|
|
|
@@ -284,7 +314,7 @@ const getFileInfo = Effect.gen(function* () {
|
|
|
284
314
|
|
|
285
315
|
yield* Console.log(`Type: ${info.type}`);
|
|
286
316
|
// "File" | "Directory" | "SymbolicLink" | "BlockDevice" | "CharacterDevice" | "FIFO" | "Socket" | "Unknown"
|
|
287
|
-
yield* Console.log(`Size: ${info.size}`); //
|
|
317
|
+
yield* Console.log(`Size: ${info.size}`); // ByteSize.ByteSize
|
|
288
318
|
yield* Console.log(`Modified: ${info.mtime}`); // Option<Date>
|
|
289
319
|
yield* Console.log(`Accessed: ${info.atime}`); // Option<Date>
|
|
290
320
|
yield* Console.log(`Created: ${info.birthtime}`); // Option<Date>
|
|
@@ -7,6 +7,21 @@ You are an Effect TypeScript expert specializing in the HttpApi module for build
|
|
|
7
7
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
|
+
Use `HttpApi.ParseOptions` annotations at API, group, or endpoint scope to
|
|
11
|
+
configure client and server codecs. `HttpApiBuilder.handler` defines a reusable
|
|
12
|
+
endpoint callback with inferred request, success, error, and service types.
|
|
13
|
+
Generated clients and AtomHttpApi calls accept per-call `sseOptions`.
|
|
14
|
+
SSE IDs may be absent: model them with `Schema.optional(Schema.String)`.
|
|
15
|
+
With excess-property errors, include the `event` field (default `message`) and
|
|
16
|
+
any `id`, including inherited IDs. `Sse.decodeSchema` / `ChannelSchema.decode`
|
|
17
|
+
accept parse options directly.
|
|
18
|
+
|
|
19
|
+
HTTP `QUERY` endpoints are supported. OpenAPI 3.1 represents them through
|
|
20
|
+
`x-oai-additionalOperations`, requiring consumer support for that extension;
|
|
21
|
+
the generator also accepts OpenAPI 3.2's native `query` operation.
|
|
22
|
+
Generated multipart binary fields use `File | Blob` and dedicated `*Multipart`
|
|
23
|
+
component exports. Keep generated imports aligned with the chosen transport.
|
|
24
|
+
|
|
10
25
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Browse and read files there directly to look up APIs, types, and implementations.
|
|
11
26
|
|
|
12
27
|
Key reference files:
|
|
@@ -1128,7 +1143,7 @@ The `layerClient` second argument can also be an `Effect` returning the middlewa
|
|
|
1128
1143
|
const AuthorizationClient = HttpApiMiddleware.layerClient(
|
|
1129
1144
|
Authorization,
|
|
1130
1145
|
Effect.gen(function* () {
|
|
1131
|
-
const token = yield* Config.
|
|
1146
|
+
const token = yield* Config.Redacted('API_TOKEN');
|
|
1132
1147
|
return ({ next, request }) =>
|
|
1133
1148
|
next(
|
|
1134
1149
|
HttpClientRequest.bearerToken(request, Redacted.value(token))
|
|
@@ -1170,7 +1185,7 @@ Top-level group endpoints are at the root: `buildUrl.health()`. With `disableCod
|
|
|
1170
1185
|
|
|
1171
1186
|
## OpenAPI Documentation
|
|
1172
1187
|
|
|
1173
|
-
|
|
1188
|
+
Built-in OpenAPI documentation responses are generated lazily on
|
|
1174
1189
|
the first request, rather than while the route layer is built. A generation
|
|
1175
1190
|
defect is not permanently cached: a later request retries generation. Include a
|
|
1176
1191
|
documentation-route request in integration checks if generation must be verified;
|
|
@@ -9,6 +9,15 @@ In v4 there is no `@effect/platform` package — the HTTP client lives in the `e
|
|
|
9
9
|
|
|
10
10
|
## Effect Source Reference
|
|
11
11
|
|
|
12
|
+
Client recovery through `HttpClient.catch` must return an `HttpClientResponse`.
|
|
13
|
+
For a different success type, recover on the Effect returned by `execute`.
|
|
14
|
+
`response.url` includes query parameters, excludes the hash, and reflects the
|
|
15
|
+
final URL after redirects. Response schema decoders honor supplied parse options.
|
|
16
|
+
Use `Schema.Cookie`, `Schema.Cookies`, `Schema.Headers`, and `Schema.UrlParams`
|
|
17
|
+
for HTTP value schemas. Import Undici-specific APIs from
|
|
18
|
+
`@effect/platform-node/Undici`; the transport layer owns their initialization.
|
|
19
|
+
HTTP `QUERY` is supported; configure CORS `allowedMethods` explicitly if needed.
|
|
20
|
+
|
|
12
21
|
The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these modules are `unstable` and change between betas.
|
|
13
22
|
|
|
14
23
|
Key files:
|
|
@@ -42,7 +51,7 @@ An `HttpClient.With<E, R>` is a pair of functions — `preprocess` (request →
|
|
|
42
51
|
- Runtime application and provider integrations use `HttpClient`; do not call raw `fetch` from business or provider code.
|
|
43
52
|
- A raw `fetch` call is permitted only in an explicitly named low-level platform adapter that owns transport interop and documents why an Effect transport cannot be used. Lift it with `Effect.tryPromise`, pass the supplied `AbortSignal` to fetch, and do not let `Request`, `Response`, rejected promises, or untyped payloads escape that adapter.
|
|
44
53
|
- Give each upstream adapter a named service and named effects that own request construction, authentication, execution, status classification, schema decoding, and error mapping.
|
|
45
|
-
- Read credentials with `Config.
|
|
54
|
+
- Read credentials with `Config.Redacted` and attach them in a configured client transform; never pass raw secret strings through business workflows.
|
|
46
55
|
- Classify status before decoding a success schema. Non-2xx error bodies often have a different shape and must not be decoded as successful payloads.
|
|
47
56
|
- Decode external response data with `HttpClientResponse.schemaBodyJson`, `schemaJson`, or another `Schema` decoder. A successful JSON parse is not validation.
|
|
48
57
|
- Preserve bounded diagnostic evidence such as provider request IDs, status, error codes, and retry metadata. Redact credentials, authorization headers, query secrets, private payload fields, and full bodies before logging or storing evidence.
|
|
@@ -233,7 +242,7 @@ request.pipe(
|
|
|
233
242
|
|
|
234
243
|
`UrlParams.Input` accepts records, iterables of `[key, value]` tuples, or `URLSearchParams`. Values may be `string | number | bigint | boolean | null | undefined`; `undefined` entries are **skipped** (great for optional params), arrays produce repeated keys, and nested records render with bracket notation (`filter[name]=x`).
|
|
235
244
|
|
|
236
|
-
For standalone `UrlParams` values (e.g. `response.urlParamsBody`) the module mirrors these combinators: `UrlParams.getFirst`/`getAll`, `set`/`append`/`setAll`/`appendAll`, `toRecord`.
|
|
245
|
+
For standalone `UrlParams` values (e.g. `response.urlParamsBody`) the module mirrors these combinators: `UrlParams.getFirst`/`getAll`, `set`/`append`/`setAll`/`appendAll`, `toRecord`. Schema values and record/JSON-field codecs live in `effect/Schema`; use `Schema.RecordFromUrlParams` and `Schema.JsonFromUrlParamsField(name, options?)`.
|
|
237
246
|
|
|
238
247
|
### Headers and auth
|
|
239
248
|
|
|
@@ -197,14 +197,14 @@ const byteStream = request.stream; // Stream<Uint8Array, HttpServerError> (singl
|
|
|
197
197
|
|
|
198
198
|
Cap accepted body sizes with the `MaxBodySize` reference (re-exported from `HttpIncomingMessage`, default `undefined` = unlimited):
|
|
199
199
|
|
|
200
|
-
On Node
|
|
200
|
+
On Node, `remoteAddress` returns `Option.none()` after Node has cleared
|
|
201
201
|
the incoming message's socket. Preserve absence; do not dereference the native
|
|
202
202
|
socket after request cleanup. The same behavior applies to Node client responses.
|
|
203
203
|
|
|
204
204
|
```ts
|
|
205
205
|
import { FileSystem } from 'effect';
|
|
206
206
|
|
|
207
|
-
someEffect.pipe(Effect.provideService(HttpServerRequest.MaxBodySize,
|
|
207
|
+
someEffect.pipe(Effect.provideService(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1)));
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
### Schema-validated decoding
|
|
@@ -624,7 +624,7 @@ Layer.launch(Main).pipe(BunRuntime.runMain);
|
|
|
624
624
|
|
|
625
625
|
```ts
|
|
626
626
|
const server = yield* HttpServer.HttpServer;
|
|
627
|
-
server.address; //
|
|
627
|
+
server.address; // NetAddress.SocketAddress; narrow to InetAddress before reading port
|
|
628
628
|
HttpServer.formatAddress(server.address); // 'http://0.0.0.0:3000'
|
|
629
629
|
yield* HttpServer.logAddress; // log it
|
|
630
630
|
someServerLayer.pipe(HttpServer.withLogAddress); // log on startup
|
|
@@ -654,7 +654,7 @@ yield* HttpServer.serveEffect(httpEffect);
|
|
|
654
654
|
|
|
655
655
|
## 8. WebSocket Upgrades
|
|
656
656
|
|
|
657
|
-
In `@effect/platform-bun
|
|
657
|
+
In `@effect/platform-bun`, outgoing WebSocket messages are compressed when
|
|
658
658
|
per-message deflate is configured **and negotiated**. The server option
|
|
659
659
|
`websocket.compressionThreshold` sets the minimum byte size (default `1024`);
|
|
660
660
|
smaller messages stay uncompressed. Configure it alongside
|
|
@@ -667,16 +667,18 @@ application payloads. See `packages/platform/bun/src/BunHttpServer.ts`.
|
|
|
667
667
|
const WsRoute = HttpRouter.add('GET', '/ws', Effect.gen(function* () {
|
|
668
668
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
669
669
|
const socket = yield* request.upgrade; // Effect<Socket.Socket, HttpServerError>
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
670
|
+
const writer = yield* socket.writer;
|
|
671
|
+
const pull = yield* Socket.readerString(socket);
|
|
672
|
+
yield* pull.pipe(
|
|
673
|
+
Effect.flatMap((batch) => Effect.forEach(batch, (message) => writer.write(`echo: ${message}`))),
|
|
674
|
+
Effect.forever,
|
|
675
|
+
Effect.catchReason('SocketError', 'SocketCloseError', () => Effect.void)
|
|
676
|
+
);
|
|
677
|
+
return HttpServerResponse.empty(); // the adapter does not write HTTP bytes after upgrade
|
|
676
678
|
}));
|
|
677
679
|
```
|
|
678
680
|
|
|
679
|
-
- `socket.
|
|
681
|
+
- Acquire `socket.reader` for raw batches or `Socket.readerBytes` / `readerString` for converted pulls. Every close is a typed failure; handle closure at the protocol boundary.
|
|
680
682
|
- `HttpServerRequest.upgradeChannel()` exposes the socket as a `Channel` for pipeline-style use.
|
|
681
683
|
- `request.upgrade` fails with `HttpServerError` (`RequestParseError` reason) when the request is not upgradeable — e.g. in plain web-handler adapters that lack upgrade support.
|
|
682
684
|
- For socket combinators, close events, and client sockets see the `effect-socket` skill.
|
|
@@ -685,6 +687,21 @@ const WsRoute = HttpRouter.add('GET', '/ws', Effect.gen(function* () {
|
|
|
685
687
|
|
|
686
688
|
## 9. Static Files (`HttpStaticServer`)
|
|
687
689
|
|
|
690
|
+
`HttpServerResponse.file` calculates length from the file and requested range;
|
|
691
|
+
it has no `contentLength` override. Explicit content types/headers take priority;
|
|
692
|
+
web files then use nonempty `File.type`, then extension inference. Ranges clamp
|
|
693
|
+
to EOF and retain exact bigint offsets. HEAD and statuses 204/205/304 omit the
|
|
694
|
+
body and finalize request resources without starting omitted Effect streams.
|
|
695
|
+
|
|
696
|
+
`HttpRouter.toWebHandler` and the layer-based `HttpEffect` web handlers start
|
|
697
|
+
building their layers when created. Construction failures reject requests with
|
|
698
|
+
the build error. Own and dispose that handler lifetime explicitly.
|
|
699
|
+
|
|
700
|
+
Bound server addresses use `NetAddress.SocketAddress`. Use `NetAddress.formatIp`
|
|
701
|
+
for numeric IP display, `formatHost` for host/port socket APIs, and the server URL
|
|
702
|
+
helpers for IPv6-safe URLs. Bun/Deno listener layers can fail with `ServeError`
|
|
703
|
+
when address conversion fails.
|
|
704
|
+
|
|
688
705
|
```ts
|
|
689
706
|
const StaticFiles = HttpStaticServer.layer({
|
|
690
707
|
root: './public',
|
|
@@ -424,7 +424,7 @@ export const DatabaseTest = Layer.succeed(
|
|
|
424
424
|
|
|
425
425
|
// Use in application
|
|
426
426
|
const program = Effect.gen(function* () {
|
|
427
|
-
const nodeEnv = yield* Config.
|
|
427
|
+
const nodeEnv = yield* Config.String('NODE_ENV').pipe(
|
|
428
428
|
Config.withDefault('production')
|
|
429
429
|
);
|
|
430
430
|
yield* myProgram.pipe(
|
|
@@ -463,6 +463,10 @@ const program = Effect.all([
|
|
|
463
463
|
|
|
464
464
|
## Error Handling in Layers
|
|
465
465
|
|
|
466
|
+
`Layer.tapError` and `Layer.tapCause` observers must accept the complete source
|
|
467
|
+
error type. Preloading a LayerMap does not make future resource acquisition
|
|
468
|
+
infallible; its accessors retain the resource error channel.
|
|
469
|
+
|
|
466
470
|
Handle construction errors:
|
|
467
471
|
|
|
468
472
|
```typescript
|
|
@@ -577,7 +581,7 @@ const layer = Layer.effect(Settings, Effect.gen(function* () {
|
|
|
577
581
|
For refresh use `Effect.cachedInvalidateWithTTL(work, Duration.infinity)` and
|
|
578
582
|
expose the returned invalidation effect. For keyed retention use `Cache`,
|
|
579
583
|
`ScopedCache`, `RcMap`, or `LayerMap` according to resource lifetime (see
|
|
580
|
-
`effect-cache`). `LayerMap.contextEffectOption`
|
|
584
|
+
`effect-cache`). `LayerMap.contextEffectOption` atomically retains an
|
|
581
585
|
already-cached layer context without allocating a missing key.
|
|
582
586
|
|
|
583
587
|
When the requirement is a restartable worker, latest-wins scheduling, or queued
|
|
@@ -53,14 +53,15 @@ Layer.mergeAll(
|
|
|
53
53
|
)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Every server runner requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback.
|
|
56
|
+
Every server runner requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback. `initialize` negotiates from its body even when the client sends an unsupported default protocol header. Subsequent requests validate `MCP-Protocol-Version`; unsupported explicit versions return `400`. Let initialization reach body negotiation.
|
|
57
57
|
|
|
58
58
|
## Protocol Revisions
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Declare the protocol adapters the server supports:
|
|
61
61
|
|
|
62
62
|
```typescript
|
|
63
63
|
const protocols = [
|
|
64
|
+
McpProtocol.v2026_07_28,
|
|
64
65
|
McpProtocol.v2025_11_25,
|
|
65
66
|
McpProtocol.v2025_06_18,
|
|
66
67
|
McpProtocol.v2025_03_26,
|
|
@@ -71,6 +72,7 @@ const protocols = [
|
|
|
71
72
|
- `2024-11-05` and `2025-03-26` are compatibility revisions.
|
|
72
73
|
- `2025-06-18` supports form elicitation.
|
|
73
74
|
- `2025-11-25` adds sampling with tools, independently advertised form/URL elicitation modes, descriptor icons, and elicitation-complete notifications.
|
|
75
|
+
- `2026-07-28` is available as `McpProtocol.v2026_07_28`.
|
|
74
76
|
- Duplicate versions or an empty protocol declaration fail layer construction with `Cause.IllegalArgumentError`.
|
|
75
77
|
- `v2024_11_05` over `layerHttp` uses Effect's single-endpoint Streamable HTTP compatibility transport. It does not recreate the historical two-endpoint HTTP+SSE transport, GET SSE, event resumption, session expiry, or client session termination.
|
|
76
78
|
|
|
@@ -78,6 +80,18 @@ Each part layer has type `Layer.Layer<never, never, ...>` — they register them
|
|
|
78
80
|
|
|
79
81
|
## Tools and Toolkit
|
|
80
82
|
|
|
83
|
+
Server options accept `instructions` for initialization/discovery. Prompt
|
|
84
|
+
registration accepts `title`; callbacks receive decoded prompt parameters.
|
|
85
|
+
|
|
86
|
+
`Tool.Strict` controls MCP input schema generation and argument validation.
|
|
87
|
+
Strict dynamic tools require Effect schemas; raw JSON Schema is rejected at
|
|
88
|
+
registration. Non-strict tools may use identified input schemas. Invalid arguments
|
|
89
|
+
produce `InvalidParams` before protocol 2025-11-25 and `isError: true` on newer
|
|
90
|
+
protocols. Declared handler failures return `isError: true` without
|
|
91
|
+
`structuredContent`; only JSON objects are valid structured content.
|
|
92
|
+
Declared failures are distinct from internal diagnostics. Defects and encoding
|
|
93
|
+
failures are logged/reported while client-facing messages stay generic.
|
|
94
|
+
|
|
81
95
|
### Defining Tools
|
|
82
96
|
|
|
83
97
|
Tools are defined with `Tool.make` specifying a name, description, parameter schemas, and success schema:
|
|
@@ -362,7 +376,9 @@ Layer.mergeAll(/* parts */).pipe(
|
|
|
362
376
|
);
|
|
363
377
|
```
|
|
364
378
|
|
|
365
|
-
|
|
379
|
+
For stdio transport, route logs to stderr with `Logger.LogToStderr`. Stdout is
|
|
380
|
+
reserved for protocol messages. `Logger.consolePretty` controls formatting;
|
|
381
|
+
the context reference controls its destination.
|
|
366
382
|
|
|
367
383
|
### HTTP Transport
|
|
368
384
|
|
|
@@ -525,7 +541,8 @@ const ServerLayer = Layer.mergeAll(
|
|
|
525
541
|
})
|
|
526
542
|
),
|
|
527
543
|
Layer.provide(NodeStdio.layer),
|
|
528
|
-
Layer.provide(Logger.layer([Logger.consolePretty(
|
|
544
|
+
Layer.provide(Logger.layer([Logger.consolePretty()])),
|
|
545
|
+
Layer.provide(Layer.succeed(Logger.LogToStderr)(true))
|
|
529
546
|
);
|
|
530
547
|
|
|
531
548
|
Layer.launch(ServerLayer).pipe(NodeRuntime.runMain);
|
|
@@ -207,7 +207,7 @@ import { Config, Effect, Layer, Logger } from 'effect';
|
|
|
207
207
|
|
|
208
208
|
const LoggerLayer = Layer.unwrap(
|
|
209
209
|
Effect.gen(function* () {
|
|
210
|
-
const env = yield* Config.
|
|
210
|
+
const env = yield* Config.String('NODE_ENV').pipe(
|
|
211
211
|
Config.withDefault('development')
|
|
212
212
|
);
|
|
213
213
|
if (env === 'production') {
|
|
@@ -652,7 +652,7 @@ const ProdObservability = Layer.mergeAll(
|
|
|
652
652
|
|
|
653
653
|
const ObservabilityLayer = Layer.unwrap(
|
|
654
654
|
Effect.gen(function* () {
|
|
655
|
-
const env = yield* Config.
|
|
655
|
+
const env = yield* Config.String('NODE_ENV').pipe(
|
|
656
656
|
Config.withDefault('development')
|
|
657
657
|
);
|
|
658
658
|
return env === 'production' ? ProdObservability : DevObservability;
|
|
@@ -328,7 +328,7 @@ const atKey = (key: string) =>
|
|
|
328
328
|
);
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
Fallible optic operations use `SchemaIssue.Issue`. Custom `makePrism` and `makeOptional` implementations return structured issues. Use `SchemaIssue.makeFormatterDefault()` for human-readable messages:
|
|
332
332
|
|
|
333
333
|
```ts
|
|
334
334
|
const formatIssue = SchemaIssue.makeFormatterDefault();
|