opencode-effect-enforcer 0.2.2 → 0.2.4
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 +38 -10
- package/docs/effect-4.0.0-rc.112.md +316 -0
- package/guidance/effect-first-development.md +30 -17
- package/guidance/progressive-disclosure-guidance.md +13 -0
- package/package.json +3 -2
- package/patterns/avoid-direct-tag-checks.md +8 -2
- package/patterns/avoid-react-hooks.md +18 -37
- package/patterns/effect-run-in-body.md +1 -1
- package/patterns/require-effect-concurrency.md +11 -0
- package/patterns/use-console-service.md +6 -1
- package/skills/effect-ai-language-model/SKILL.md +10 -16
- package/skills/effect-ai-prompt/SKILL.md +36 -2
- package/skills/effect-ai-provider/SKILL.md +13 -0
- package/skills/effect-ai-streaming/SKILL.md +81 -108
- package/skills/effect-ai-tool/SKILL.md +50 -87
- package/skills/effect-atom-rpc/SKILL.md +9 -2
- package/skills/effect-atom-state/SKILL.md +5 -0
- package/skills/effect-cache/SKILL.md +32 -0
- package/skills/effect-cli/SKILL.md +22 -3
- package/skills/effect-concurrency-testing/SKILL.md +7 -9
- package/skills/effect-domain-modeling/SKILL.md +208 -1169
- package/skills/effect-domain-predicates/SKILL.md +5 -6
- package/skills/effect-error-handling/SKILL.md +5 -4
- package/skills/effect-http-api/SKILL.md +12 -1
- package/skills/effect-http-client/SKILL.md +1 -1
- package/skills/effect-http-server/SKILL.md +14 -3
- package/skills/effect-layer-design/SKILL.md +22 -56
- package/skills/effect-mcp-server/SKILL.md +1 -1
- package/skills/effect-pattern-matching/SKILL.md +44 -11
- package/skills/effect-platform-abstraction/SKILL.md +1 -1
- package/skills/effect-platform-layers/SKILL.md +1 -1
- package/skills/effect-rpc-api/SKILL.md +8 -1
- package/skills/effect-rpc-client/SKILL.md +20 -6
- package/skills/effect-rpc-cluster/SKILL.md +44 -14
- package/skills/effect-rpc-server/SKILL.md +32 -5
- package/skills/effect-scheduling/SKILL.md +1 -1
- package/skills/effect-schema-composition/SKILL.md +69 -15
- package/skills/effect-schema-v4/SKILL.md +43 -1
- package/skills/effect-scope/SKILL.md +30 -0
- package/skills/effect-service-implementation/SKILL.md +10 -4
- package/skills/effect-socket/SKILL.md +5 -5
- package/skills/effect-sql/SKILL.md +22 -0
- package/skills/effect-stream/SKILL.md +32 -1
- package/skills/effect-testing/SKILL.md +39 -31
- package/skills/effect-workflow/SKILL.md +6 -0
- package/patterns/vm-in-wrong-file.md +0 -51
- package/skills/effect-react-vm/SKILL.md +0 -675
|
@@ -55,8 +55,7 @@ When you need an `Equivalence` instance (for use with combinators), derive it fr
|
|
|
55
55
|
import { Schema, Array } from 'effect';
|
|
56
56
|
import * as Equivalence from 'effect/Equivalence';
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
type Task = Schema.Schema.Type<typeof Task>;
|
|
58
|
+
class Task extends Schema.Class<Task>('Task')({ id: Schema.String }) {}
|
|
60
59
|
|
|
61
60
|
// Derive from schema (structural equality)
|
|
62
61
|
export const TaskEquivalence = Schema.toEquivalence(Task);
|
|
@@ -603,7 +602,7 @@ declare const appointments: Array<Appointment.Appointment>;
|
|
|
603
602
|
* import { pipe } from "effect/Function"
|
|
604
603
|
*
|
|
605
604
|
* const tomorrow = DateTime.addDuration(
|
|
606
|
-
* DateTime.
|
|
605
|
+
* DateTime.makeUnsafe('2026-09-06T00:00:00Z'),
|
|
607
606
|
* Duration.days(1)
|
|
608
607
|
* )
|
|
609
608
|
*
|
|
@@ -612,7 +611,8 @@ declare const appointments: Array<Appointment.Appointment>;
|
|
|
612
611
|
* Array.filter(Appointment.isScheduledBefore(tomorrow))
|
|
613
612
|
* )
|
|
614
613
|
*/
|
|
615
|
-
|
|
614
|
+
// Deterministic fixture; in runtime code obtain the instant with yield* DateTime.now.
|
|
615
|
+
const tomorrow = DateTime.addDuration(DateTime.makeUnsafe('2026-09-06T00:00:00Z'), Duration.days(1));
|
|
616
616
|
|
|
617
617
|
const beforeTomorrow = pipe(
|
|
618
618
|
appointments,
|
|
@@ -791,8 +791,7 @@ const areSame = Equal.equals(t1, t2);
|
|
|
791
791
|
```typescript
|
|
792
792
|
import { Schema, Array } from 'effect';
|
|
793
793
|
|
|
794
|
-
|
|
795
|
-
type Task = Schema.Schema.Type<typeof Task>;
|
|
794
|
+
class Task extends Schema.Class<Task>('Task')({ id: Schema.String }) {}
|
|
796
795
|
|
|
797
796
|
export const Equivalence = Schema.toEquivalence(Task);
|
|
798
797
|
|
|
@@ -1083,7 +1083,7 @@ const secondaryService: Effect.Effect<Data, SecondaryServiceError> =
|
|
|
1083
1083
|
|
|
1084
1084
|
// Try primary, fallback to secondary
|
|
1085
1085
|
// Effect<Data, SecondaryServiceError, Dependencies>
|
|
1086
|
-
const program = primaryService.pipe(Effect.
|
|
1086
|
+
const program = primaryService.pipe(Effect.catch(() => secondaryService));
|
|
1087
1087
|
```
|
|
1088
1088
|
|
|
1089
1089
|
### Retry with Schedule
|
|
@@ -1205,10 +1205,11 @@ const program = loadConfig.pipe(
|
|
|
1205
1205
|
|
|
1206
1206
|
// With custom defect message
|
|
1207
1207
|
const program2 = loadConfig.pipe(
|
|
1208
|
-
Effect.
|
|
1208
|
+
Effect.mapError(
|
|
1209
1209
|
(error) =>
|
|
1210
1210
|
new Error(`Fatal: Configuration failed to load: ${error._tag}`)
|
|
1211
|
-
)
|
|
1211
|
+
),
|
|
1212
|
+
Effect.orDie
|
|
1212
1213
|
);
|
|
1213
1214
|
```
|
|
1214
1215
|
|
|
@@ -1387,7 +1388,7 @@ Model ambiguous HTTP responses (where the body structure differs for success vs
|
|
|
1387
1388
|
|
|
1388
1389
|
```typescript
|
|
1389
1390
|
import { Effect, Schema } from 'effect';
|
|
1390
|
-
import { HttpClientResponse } from 'effect/unstable/
|
|
1391
|
+
import { HttpClientResponse } from 'effect/unstable/http';
|
|
1391
1392
|
|
|
1392
1393
|
class TokenSuccess extends Schema.Class<TokenSuccess>('TokenSuccess')({
|
|
1393
1394
|
access_token: AccessToken,
|
|
@@ -14,7 +14,7 @@ Key reference files:
|
|
|
14
14
|
- `packages/effect/HTTPAPI.md` — canonical HttpApi documentation
|
|
15
15
|
- `packages/effect/src/unstable/httpapi/*.ts` — module sources
|
|
16
16
|
- `packages/effect/typetest/unstable/httpapi/*.tst.ts` — type-level contracts
|
|
17
|
-
- `packages/platform
|
|
17
|
+
- `packages/platform/node/test/HttpApi.test.ts` — comprehensive runtime tests
|
|
18
18
|
- `ai-docs/src/51_http-server/` — server walkthrough with fixtures
|
|
19
19
|
- `ai-docs/src/50_http-client/` — HttpClient walkthrough
|
|
20
20
|
|
|
@@ -1170,6 +1170,17 @@ Top-level group endpoints are at the root: `buildUrl.health()`. With `disableCod
|
|
|
1170
1170
|
|
|
1171
1171
|
## OpenAPI Documentation
|
|
1172
1172
|
|
|
1173
|
+
As of rc.112 built-in OpenAPI documentation responses are generated lazily on
|
|
1174
|
+
the first request, rather than while the route layer is built. A generation
|
|
1175
|
+
defect is not permanently cached: a later request retries generation. Include a
|
|
1176
|
+
documentation-route request in integration checks if generation must be verified;
|
|
1177
|
+
successful server startup alone no longer proves it. Explicit `OpenApi.fromApi`
|
|
1178
|
+
remains a direct synchronous generation call.
|
|
1179
|
+
|
|
1180
|
+
JSON Schema import/conversion now rejects unsupported references, validation
|
|
1181
|
+
keywords, and unrepresentable dialect conversions instead of weakening them.
|
|
1182
|
+
See `effect-schema-v4` before importing third-party schemas into an API contract.
|
|
1183
|
+
|
|
1173
1184
|
### Scalar UI
|
|
1174
1185
|
|
|
1175
1186
|
```ts
|
|
@@ -24,7 +24,7 @@ Key files:
|
|
|
24
24
|
- `packages/effect/src/unstable/http/Url.ts` — immutable helpers over the native `URL`
|
|
25
25
|
- `packages/effect/src/unstable/http/Cookies.ts` — cookie model, `fromSetCookie`, `toCookieHeader`, `getValue`
|
|
26
26
|
- `packages/effect/src/unstable/http/Headers.ts` — header model, `Input` forms, `CurrentRedactedNames`
|
|
27
|
-
- `packages/platform
|
|
27
|
+
- `packages/platform/node/src/NodeHttpClient.ts` — Node transports: undici, node:http, fetch re-export
|
|
28
28
|
- `packages/effect/test/unstable/http/HttpClient.test.ts` — retryTransient, withRateLimiter, abort semantics
|
|
29
29
|
- `ai-docs/src/50_http-client/10_basics.ts` — canonical "wrap a configured client in a service" lesson
|
|
30
30
|
|
|
@@ -24,9 +24,9 @@ Key files:
|
|
|
24
24
|
- `packages/effect/src/unstable/http/HttpBody.ts` — body variants (`Empty`/`Raw`/`Uint8Array`/`FormData`/`Stream`) and constructors
|
|
25
25
|
- `packages/effect/src/unstable/http/Headers.ts`, `Cookies.ts`, `Multipart.ts` — header/cookie/multipart models and limits
|
|
26
26
|
- `packages/effect/src/unstable/http/HttpStaticServer.ts` — static file serving
|
|
27
|
-
- `packages/platform
|
|
28
|
-
- `packages/platform
|
|
29
|
-
- `packages/platform
|
|
27
|
+
- `packages/platform/node/src/NodeHttpServer.ts` — Node server adapter, `layer`, `layerTest`, graceful shutdown
|
|
28
|
+
- `packages/platform/bun/src/BunHttpServer.ts` — Bun equivalent
|
|
29
|
+
- `packages/platform/node/test/NodeHttpServer.test.ts` — the best end-to-end reference for real route/middleware/multipart wiring
|
|
30
30
|
|
|
31
31
|
## Core Model
|
|
32
32
|
|
|
@@ -197,6 +197,10 @@ 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 in rc.112, `remoteAddress` returns `Option.none()` after Node has cleared
|
|
201
|
+
the incoming message's socket. Preserve absence; do not dereference the native
|
|
202
|
+
socket after request cleanup. The same behavior applies to Node client responses.
|
|
203
|
+
|
|
200
204
|
```ts
|
|
201
205
|
import { FileSystem } from 'effect';
|
|
202
206
|
|
|
@@ -650,6 +654,13 @@ yield* HttpServer.serveEffect(httpEffect);
|
|
|
650
654
|
|
|
651
655
|
## 8. WebSocket Upgrades
|
|
652
656
|
|
|
657
|
+
In `@effect/platform-bun` rc.112, outgoing WebSocket messages are compressed when
|
|
658
|
+
per-message deflate is configured **and negotiated**. The server option
|
|
659
|
+
`websocket.compressionThreshold` sets the minimum byte size (default `1024`);
|
|
660
|
+
smaller messages stay uncompressed. Configure it alongside
|
|
661
|
+
`websocket.perMessageDeflate` on `BunHttpServer.layer` rather than pre-compressing
|
|
662
|
+
application payloads. See `packages/platform/bun/src/BunHttpServer.ts`.
|
|
663
|
+
|
|
653
664
|
`request.upgrade` yields a `Socket` (from `effect/unstable/socket`) once the connection is upgraded. Both `NodeHttpServer` and `BunHttpServer` handle the platform `upgrade` events for you — just write a normal route:
|
|
654
665
|
|
|
655
666
|
```ts
|
|
@@ -554,69 +554,35 @@ const services = Layer.mergeAll(UserServiceLayer, OrderServiceLayer).pipe(
|
|
|
554
554
|
|
|
555
555
|
If downstream code does not need the dependency, use `Layer.provide` and keep it hidden. Do not preserve every intermediate service by default.
|
|
556
556
|
|
|
557
|
-
## Pattern:
|
|
557
|
+
## Pattern: Share Work With a Cache
|
|
558
558
|
|
|
559
|
-
For
|
|
559
|
+
For lazy one-shot result sharing, allocate `Effect.cached(work)` once inside the
|
|
560
|
+
service's layer. It shares in-flight work and the completed result (including
|
|
561
|
+
failure). Do not reallocate the cache on each method call.
|
|
560
562
|
|
|
563
|
+
<!-- typecheck -->
|
|
561
564
|
```typescript
|
|
562
|
-
import {
|
|
563
|
-
|
|
564
|
-
type State<A, E> =
|
|
565
|
-
| { readonly _tag: 'Idle' }
|
|
566
|
-
| {
|
|
567
|
-
readonly _tag: 'Running';
|
|
568
|
-
readonly done: Deferred.Deferred<A, E>;
|
|
569
|
-
readonly fiber: Fiber.Fiber<A, E>;
|
|
570
|
-
}
|
|
571
|
-
| { readonly _tag: 'Pending'; readonly done: Deferred.Deferred<A, E> };
|
|
572
|
-
|
|
573
|
-
const make = <A, E>(scope: Scope.Scope) => {
|
|
574
|
-
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: 'Idle' });
|
|
575
|
-
|
|
576
|
-
const run = (work: Effect.Effect<A, E>) =>
|
|
577
|
-
SynchronizedRef.modifyEffect(
|
|
578
|
-
ref,
|
|
579
|
-
Effect.fnUntraced(function* (state) {
|
|
580
|
-
switch (state._tag) {
|
|
581
|
-
case 'Running':
|
|
582
|
-
// Already running — share the existing result
|
|
583
|
-
return [Deferred.await(state.done), state];
|
|
584
|
-
case 'Idle': {
|
|
585
|
-
// Start new work
|
|
586
|
-
const done = yield* Deferred.make<A, E>();
|
|
587
|
-
const fiber = yield* Effect.forkIn(
|
|
588
|
-
work.pipe(Effect.intoDeferred(done)),
|
|
589
|
-
scope
|
|
590
|
-
);
|
|
591
|
-
return [
|
|
592
|
-
Deferred.await(done),
|
|
593
|
-
{ _tag: 'Running' as const, done, fiber }
|
|
594
|
-
];
|
|
595
|
-
}
|
|
596
|
-
case 'Pending': {
|
|
597
|
-
// Queued — share the pending result
|
|
598
|
-
return [Deferred.await(state.done), state];
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
})
|
|
602
|
-
).pipe(Effect.flatten);
|
|
603
|
-
|
|
604
|
-
return { run };
|
|
605
|
-
};
|
|
606
|
-
```
|
|
565
|
+
import { Context, Effect, Layer } from 'effect';
|
|
607
566
|
|
|
608
|
-
|
|
567
|
+
class Settings extends Context.Service<Settings, {
|
|
568
|
+
readonly load: Effect.Effect<string>;
|
|
569
|
+
}>()('app/Settings') {}
|
|
609
570
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
571
|
+
const layer = Layer.effect(Settings, Effect.gen(function* () {
|
|
572
|
+
const load = yield* Effect.cached(Effect.succeed('settings'));
|
|
573
|
+
return Settings.of({ load });
|
|
574
|
+
}));
|
|
575
|
+
```
|
|
614
576
|
|
|
615
|
-
|
|
577
|
+
For refresh use `Effect.cachedInvalidateWithTTL(work, Duration.infinity)` and
|
|
578
|
+
expose the returned invalidation effect. For keyed retention use `Cache`,
|
|
579
|
+
`ScopedCache`, `RcMap`, or `LayerMap` according to resource lifetime (see
|
|
580
|
+
`effect-cache`). `LayerMap.contextEffectOption` in rc.112 atomically retains an
|
|
581
|
+
already-cached layer context without allocating a missing key.
|
|
616
582
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
-
|
|
583
|
+
When the requirement is a restartable worker, latest-wins scheduling, or queued
|
|
584
|
+
state transitions, use a dedicated coordinator with explicit states and a
|
|
585
|
+
supervised fiber lifetime (see `effect-fiber`). A cache is not a job scheduler.
|
|
620
586
|
|
|
621
587
|
## Naming Convention
|
|
622
588
|
|
|
@@ -53,7 +53,7 @@ Layer.mergeAll(
|
|
|
53
53
|
)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Every server runner
|
|
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. In rc.112, `initialize` negotiates from its body even if the client sends an unsupported default `MCP-Protocol-Version` header. The header is checked only on subsequent requests; unsupported explicit versions there still return `400`. Do not reject the initialization request in custom middleware before body negotiation.
|
|
57
57
|
|
|
58
58
|
## Protocol Revisions
|
|
59
59
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: effect-pattern-matching
|
|
3
|
-
description:
|
|
3
|
+
description: Match schema tagged unions with match/matchOrElse and guards, trusted Data.TaggedEnum values with $match/$is, and Effect outcomes with Effect.match. Use for discriminated unions, ADTs, or conditional logic based on tagged types.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Effect Pattern Matching Skill
|
|
@@ -28,9 +28,44 @@ Reference this for:
|
|
|
28
28
|
- Declarative, not imperative
|
|
29
29
|
- Pipeline-friendly composition
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Schema-First Matching (rc.112)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
For domain/wire models, prefer class variants combined with
|
|
34
|
+
`Schema.Union([...]).pipe(Schema.toTaggedUnion('kind'))`. Use `.match` for
|
|
35
|
+
exhaustiveness, `.guards` for narrowing decoded values, and `.matchOrElse` for
|
|
36
|
+
intentional partial matching. Direct `Schema.TaggedUnion` creates canonical
|
|
37
|
+
`_tag` object variants. Both expose data-first and data-last `matchOrElse` forms;
|
|
38
|
+
the `toTaggedUnion` fallback excludes handled variants, while direct
|
|
39
|
+
`Schema.TaggedUnion.matchOrElse` types its fallback as the full union.
|
|
40
|
+
|
|
41
|
+
<!-- typecheck -->
|
|
42
|
+
```typescript
|
|
43
|
+
import * as Schema from 'effect/Schema';
|
|
44
|
+
|
|
45
|
+
const State = Schema.TaggedUnion({
|
|
46
|
+
Loading: {},
|
|
47
|
+
Ready: { data: Schema.Array(Schema.String) },
|
|
48
|
+
Failed: { message: Schema.String }
|
|
49
|
+
});
|
|
50
|
+
type State = typeof State.Type;
|
|
51
|
+
|
|
52
|
+
const getData = State.matchOrElse({ Ready: (state) => state.data }, () => []);
|
|
53
|
+
const label = State.match({
|
|
54
|
+
Loading: () => 'Loading',
|
|
55
|
+
Ready: ({ data }) => `${data.length} items`,
|
|
56
|
+
Failed: ({ message }) => message
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
TypeScript correctly narrows literal discriminator checks. Prefer these helpers
|
|
61
|
+
for exhaustive, reusable branching, not because manual checks cannot narrow.
|
|
62
|
+
For the full class-based lifecycle example see `effect-domain-modeling`.
|
|
63
|
+
|
|
64
|
+
## Pattern 1: Data.TaggedEnum for Trusted ADTs
|
|
65
|
+
|
|
66
|
+
Use `Data.TaggedEnum` for trusted, non-schema ADTs. It supplies constructors and
|
|
67
|
+
tag checks but does not parse unknown input; avoid a parallel Data model for an
|
|
68
|
+
existing schema union.
|
|
34
69
|
|
|
35
70
|
### The Problem: Manual Tagged Unions
|
|
36
71
|
|
|
@@ -415,14 +450,12 @@ type LoadState = Data.TaggedEnum<{
|
|
|
415
450
|
}>;
|
|
416
451
|
const LoadState = Data.taggedEnum<LoadState>();
|
|
417
452
|
|
|
418
|
-
const getData = (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
(ready) => (ready ? ready.data : [])
|
|
425
|
-
);
|
|
453
|
+
const getData = LoadState.$match({
|
|
454
|
+
Loading: () => [],
|
|
455
|
+
Ready: ({ data }) => data,
|
|
456
|
+
Error: () => []
|
|
457
|
+
});
|
|
458
|
+
// A guard returns boolean; piping through $is would lose the original value.
|
|
426
459
|
```
|
|
427
460
|
|
|
428
461
|
## Pattern 5: Use Option.match Instead of \_tag Checks
|
|
@@ -16,7 +16,7 @@ Reference this for:
|
|
|
16
16
|
- Path source: `packages/effect/src/Path.ts`
|
|
17
17
|
- Crypto source: `packages/effect/src/Crypto.ts`
|
|
18
18
|
- Socket source: `packages/effect/src/unstable/socket/`
|
|
19
|
-
- Platform layers: `packages/platform
|
|
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/`
|
|
22
22
|
|
|
@@ -292,7 +292,7 @@ const TestContext = Layer.mergeAll(
|
|
|
292
292
|
Terminal.make({
|
|
293
293
|
columns: Effect.succeed(80),
|
|
294
294
|
rows: Effect.succeed(24),
|
|
295
|
-
|
|
295
|
+
readInput: Effect.die('readInput not used in this test'),
|
|
296
296
|
readLine: Effect.succeed('test input'),
|
|
297
297
|
display: () => Effect.void
|
|
298
298
|
})
|
|
@@ -9,6 +9,13 @@ This skill covers the **contract layer**: the definitions that client and server
|
|
|
9
9
|
|
|
10
10
|
## Effect Source Reference
|
|
11
11
|
|
|
12
|
+
In rc.112 schema encoding is selected by the transport's `codecFor`, rather
|
|
13
|
+
than being fixed to canonical JSON for every protocol. Preserve the schemas and
|
|
14
|
+
their decoding/encoding requirements in shared contracts; custom protocol
|
|
15
|
+
implementations must supply `codecFor` (see `effect-rpc-client` /
|
|
16
|
+
`effect-rpc-server`). Existing JSON, NDJSON, JSON-RPC, and MsgPack formats keep
|
|
17
|
+
their wire representation; the new SchemaBinary transport has its own codecs.
|
|
18
|
+
|
|
12
19
|
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 under `unstable` and move between betas.
|
|
13
20
|
|
|
14
21
|
Key files:
|
|
@@ -20,7 +27,7 @@ Key files:
|
|
|
20
27
|
- `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire envelopes: `Request`, `Ack`, `Interrupt`, `Eof`, `Ping`, `ResponseChunk`, `ResponseExit`, `ExitEncoded`, `RequestId`
|
|
21
28
|
- `packages/effect/src/unstable/rpc/RpcClientError.ts` — the transport error type referenced when typing shared client aliases
|
|
22
29
|
- `packages/effect/src/unstable/rpc/index.ts` — public exports of the rpc namespace
|
|
23
|
-
- `packages/platform
|
|
30
|
+
- `packages/platform/node/test/fixtures/rpc-schemas.ts` — the best real-world contract fixture: rpcs, streaming, middleware, deferred responses
|
|
24
31
|
- `packages/effect/test/rpc/Rpc.test.ts` — `exitSchema`, custom defect schemas, `getStreamSchemas` semantics
|
|
25
32
|
|
|
26
33
|
## Core Model
|
|
@@ -20,8 +20,8 @@ Key files:
|
|
|
20
20
|
- `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage`, `layerInitialMessage`, `initialMessage`
|
|
21
21
|
- `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire vocabulary: `Request`, `Ack`, `Interrupt`, `Chunk`, `Exit`, `Defect`, `Ping`/`Pong`, `ClientProtocolError`, `RequestId`
|
|
22
22
|
- `packages/effect/src/unstable/socket/Socket.ts` — `Socket` service, `layerWebSocket`, `WebSocketConstructor`
|
|
23
|
-
- `packages/platform
|
|
24
|
-
- `packages/platform
|
|
23
|
+
- `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-e2e.ts` + `test/fixtures/rpc-schemas.ts` — the best end-to-end reference: every transport × serialization combination, headers, streams, interrupts, defects
|
|
24
|
+
- `packages/platform/browser/test/RpcWorker.test.ts` + `test/fixtures/rpc-worker.ts` — worker transport end to end
|
|
25
25
|
|
|
26
26
|
## Core Model
|
|
27
27
|
|
|
@@ -293,6 +293,13 @@ const TcpProtocolLive = RpcClient.layerProtocolSocket().pipe(
|
|
|
293
293
|
|
|
294
294
|
Build your own with `RpcClient.Protocol.make((writeResponse, clientIds) => Effect<Omit<Service, 'run'>>)` — it buffers server responses per client until that client's `run` loop is installed. You rarely need this; prefer `RpcTest` for in-memory wiring (section 11).
|
|
295
295
|
|
|
296
|
+
As of rc.112 the returned service must include `codecFor`, normally forwarded
|
|
297
|
+
from the selected `RpcSerialization` service. It chooses the schema codec for
|
|
298
|
+
RPC payloads/results while the protocol handles envelopes. JSON-compatible
|
|
299
|
+
custom transports can use `codecFor: Schema.toCodecJson`. Preserve the schema's
|
|
300
|
+
decoding and encoding service requirements; see `effect-rpc-server` for the
|
|
301
|
+
full `RpcSerialization.CodecFor` contract.
|
|
302
|
+
|
|
296
303
|
---
|
|
297
304
|
|
|
298
305
|
## 5. Serialization — Pairing Codecs with Transports
|
|
@@ -303,7 +310,14 @@ Build your own with `RpcClient.Protocol.make((writeResponse, clientIds) => Effec
|
|
|
303
310
|
|---|---|---|---|
|
|
304
311
|
| `RpcSerialization.layerJson` | `application/json` | no | HTTP (response decoded once, as an array); WebSocket (each ws message is one frame already) |
|
|
305
312
|
| `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | anything — the safe default; enables streaming over HTTP |
|
|
306
|
-
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary
|
|
313
|
+
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary framing with JSON-compatible schema codecs; msgpackr `useRecords: true` |
|
|
314
|
+
| `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | schema-derived binary framing and payload codecs; pair on both peers |
|
|
315
|
+
|
|
316
|
+
`layerSchemaBinary({ maxFrameSize?, fingerprintPayloads? })` defaults to a
|
|
317
|
+
16 MiB frame limit and no payload fingerprint. Envelopes are fingerprinted and
|
|
318
|
+
dictionary-enabled; payload fingerprints opt into strict layout agreement.
|
|
319
|
+
Existing built-in formats retain their wire encoding in rc.112. Workers use
|
|
320
|
+
`Schema.toCodecJson` with structured clone without a serialization layer.
|
|
307
321
|
| `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop over HTTP/WebSocket; maps rpc tag ↔ `method`, supports batch arrays |
|
|
308
322
|
| `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes (newline) | JSON-RPC 2.0 over sockets |
|
|
309
323
|
|
|
@@ -413,7 +427,7 @@ const getUser = (id: string) =>
|
|
|
413
427
|
Semantics to remember:
|
|
414
428
|
|
|
415
429
|
- **`ClientProtocolError` fails everything in flight.** When a socket dies or a worker crashes, every pending request on that connection fails with the same `RpcClientError`. Requests are *not* replayed after reconnect — retry at the call site.
|
|
416
|
-
- **Server defects are `Cause.Die`, not typed failures.** `Effect.catchTag` will not see them; use `Effect.sandbox`/`Effect.
|
|
430
|
+
- **Server defects are `Cause.Die`, not typed failures.** `Effect.catchTag` will not see them; use `Effect.sandbox`/`Effect.catchCause`. What survives the wire depends on the rpc's `defect` schema (see the effect-rpc-api skill).
|
|
417
431
|
- **Whole-connection defects** (server-side fatal defects when the server runs without `disableFatalDefects`) arrive as a `Defect` message and kill every in-flight request on the connection as `Cause.Die`.
|
|
418
432
|
- `RpcTest.makeClient` clients have `E = never` — no transport error channel, only your rpc errors and middleware errors.
|
|
419
433
|
|
|
@@ -548,7 +562,7 @@ it.effect('GetUser', () =>
|
|
|
548
562
|
|
|
549
563
|
Signature: `RpcTest.makeClient(group, options?: { flatten? })` with required context `Scope | Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs> | Rpc.MiddlewareClient<Rpcs>` — i.e. the handler layers, any *server* middleware layers, **and** any client middleware layers. Forgetting the client middleware layer is the classic confusing type error.
|
|
550
564
|
|
|
551
|
-
The test client's `E` is `never`: transport errors cannot occur, so tests exercise only your declared errors, middleware errors, and defects. To also test serialization and transport semantics, build a real client+server pair against `NodeHttpServer.layerTest` exactly as `packages/platform
|
|
565
|
+
The test client's `E` is `never`: transport errors cannot occur, so tests exercise only your declared errors, middleware errors, and defects. To also test serialization and transport semantics, build a real client+server pair against `NodeHttpServer.layerTest` exactly as `packages/platform/node/test/RpcServer.test.ts` does.
|
|
552
566
|
|
|
553
567
|
### `RpcClient.makeNoSerialization` (advanced)
|
|
554
568
|
|
|
@@ -656,7 +670,7 @@ const WorkerClientLive = UsersClient.layer.pipe(
|
|
|
656
670
|
5. **Mismatched client/server serialization.** Both sides share one `RpcSerialization` choice; there is no negotiation. Garbled `RpcClientDefect: Error decoding ...` errors usually mean the codecs differ.
|
|
657
671
|
6. **Reading mixed-case header names server-side.** `Headers.fromInput` lowercases keys: send `{ userId: '123' }`, read `headers.userid`. Same for `withHeaders` and middleware `Headers.set`.
|
|
658
672
|
7. **Expecting `discard: true` to be error-free.** It only removes the rpc's *declared* error; `RpcClientError` and middleware errors remain, and over HTTP the POST round-trip is still awaited.
|
|
659
|
-
8. **Catching server defects with `catchTag`.** Handler `Effect.die`s arrive as `Cause.Die`, not typed failures. Use `Effect.sandbox`/`Effect.
|
|
673
|
+
8. **Catching server defects with `catchTag`.** Handler `Effect.die`s arrive as `Cause.Die`, not typed failures. Use `Effect.sandbox`/`Effect.catchCause`, and remember one server fatal defect can fail *all* in-flight requests on the connection.
|
|
660
674
|
9. **Assuming requests survive a reconnect.** A socket/worker failure fails every in-flight call with `RpcClientError`; after reconnect nothing is replayed. Add `Effect.retry` at call sites; `retryTransientErrors: true` only keeps requests pending across *connection-establishment* failures.
|
|
661
675
|
10. **Passing `retryPolicy` to `layerProtocolSocket`.** The layer accepts `retryTransientErrors` and `onTransientError`, but not `retryPolicy`. For a custom reconnect schedule use `Layer.effect(RpcClient.Protocol)(RpcClient.makeProtocolSocket({ retryPolicy, retryTransientErrors, onTransientError }))`.
|
|
662
676
|
11. **Treating a flattened client like an object client.** With `flatten: true` you call `client('GetUser', payload)`; `client.GetUser(payload)` is not a function. Pick one shape per client.
|
|
@@ -36,9 +36,9 @@ Key files:
|
|
|
36
36
|
- `packages/effect/src/unstable/workflow/WorkflowProxy.ts` + `WorkflowProxyServer.ts` — workflow ↔ RPC/HTTP bridge
|
|
37
37
|
- `packages/effect/src/unstable/cluster/ClusterWorkflowEngine.ts` — production workflow engine backed by sharding + storage
|
|
38
38
|
- `packages/effect/src/unstable/reactivity/AtomRpc.ts` — reactive RPC client for Atom UIs (see also `effect-atom-rpc` skill)
|
|
39
|
-
- `packages/platform
|
|
40
|
-
- `packages/platform
|
|
41
|
-
- `packages/platform
|
|
39
|
+
- `packages/platform/node/src/NodeClusterHttp.ts` / `NodeClusterSocket.ts` — Node "all-in-one" cluster layers
|
|
40
|
+
- `packages/platform/bun/src/BunClusterHttp.ts` / `BunClusterSocket.ts` — Bun equivalents
|
|
41
|
+
- `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — best end-to-end reference for real RPC wiring
|
|
42
42
|
- `packages/effect/test/cluster/TestEntity.ts` + `test/cluster/Entity.test.ts` — best reference for Entity + makeTestClient
|
|
43
43
|
|
|
44
44
|
## Imports
|
|
@@ -606,7 +606,10 @@ client.Subscribe({ topic: 't' }, {
|
|
|
606
606
|
});
|
|
607
607
|
```
|
|
608
608
|
|
|
609
|
-
`discard: true`
|
|
609
|
+
`discard: true` skips response decoding and removes response-side failures;
|
|
610
|
+
transport and required client-middleware failures can still occur. A successful
|
|
611
|
+
send is not proof that the server completed the operation. Cluster persistent
|
|
612
|
+
messages additionally have their documented storage/delivery outcomes.
|
|
610
613
|
|
|
611
614
|
`asQueue: true` is useful when you need finer control than a `Stream` gives you — e.g., you want to take only one chunk, then drop it. The end-of-stream signal is `Cause.Done` in the queue's error channel.
|
|
612
615
|
|
|
@@ -685,15 +688,21 @@ const ConnectionHooksLayer = Layer.succeed(RpcClient.ConnectionHooks, {
|
|
|
685
688
|
|
|
686
689
|
### `RpcSchema.ClientAbort`
|
|
687
690
|
|
|
688
|
-
When a client interrupts a
|
|
691
|
+
When a client interrupts a subscription, inspect interrupt reasons in the exit
|
|
692
|
+
cause for the `ClientAbort` annotation. `onInterrupt` receives interruptor IDs,
|
|
693
|
+
not a Cause; use `onExit` to distinguish client cancel from server shutdown:
|
|
689
694
|
|
|
695
|
+
<!-- typecheck -->
|
|
690
696
|
```ts
|
|
691
697
|
import { RpcSchema } from 'effect/unstable/rpc';
|
|
692
|
-
import { Cause,
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
698
|
+
import { Cause, Effect, Exit, Stream } from 'effect';
|
|
699
|
+
import * as Arr from 'effect/Array';
|
|
700
|
+
|
|
701
|
+
declare const stream: Stream.Stream<string>;
|
|
702
|
+
const subscribeHandler = stream.pipe(Stream.runDrain,
|
|
703
|
+
Effect.onExit((exit) => {
|
|
704
|
+
const isClientAbort = Exit.isFailure(exit) && Arr.some(exit.cause.reasons, (reason) =>
|
|
705
|
+
Cause.isInterruptReason(reason) && reason.annotations.has(RpcSchema.ClientAbort.key));
|
|
697
706
|
return Effect.logInfo('subscribe ended', { isClientAbort });
|
|
698
707
|
})
|
|
699
708
|
);
|
|
@@ -782,10 +791,17 @@ The choice of serialization is load-bearing because of *framing*. Some transport
|
|
|
782
791
|
| Layer | Content-Type | Framed? | Use for | Notes |
|
|
783
792
|
|---|---|---|---|---|
|
|
784
793
|
| `RpcSerialization.layerJson` | `application/json` | no | `layerProtocolHttp` | Default JSON over request/response |
|
|
785
|
-
| `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | `layerProtocolWebsocket`, sockets, http+stream | Newline-delimited JSON;
|
|
794
|
+
| `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | `layerProtocolWebsocket`, sockets, http+stream | Newline-delimited JSON; one of several streaming formats |
|
|
786
795
|
| `RpcSerialization.layerJsonRpc()` | `application/json` (configurable) | no | JSON-RPC 2.0 interop | Maps `_tag` to `method`; preserves batched arrays |
|
|
787
796
|
| `RpcSerialization.layerNdJsonRpc()` | `application/json-rpc` (configurable) | yes (newline) | JSON-RPC 2.0 over sockets | |
|
|
788
|
-
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary transports |
|
|
797
|
+
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary transports | JSON-compatible schema codecs; uses `useRecords: true` |
|
|
798
|
+
| `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | binary transports, framed HTTP | schema-aware binary payloads and envelopes |
|
|
799
|
+
|
|
800
|
+
In rc.112 serializations and both protocol services require `codecFor`.
|
|
801
|
+
Custom protocols forward it from the serialization; custom JSON-compatible
|
|
802
|
+
protocols can use `Schema.toCodecJson`. Cluster network codecs now follow the
|
|
803
|
+
transport while persistent message storage remains JSON. See `effect-rpc-server`
|
|
804
|
+
for the complete contract, binary options, and compatibility constraints.
|
|
789
805
|
|
|
790
806
|
`RpcSerialization.makeMsgPack(options?)` lets you customize msgpackr (`useRecords`, `useFloat32`, etc.).
|
|
791
807
|
|
|
@@ -793,7 +809,7 @@ Picking the wrong one is a real bug:
|
|
|
793
809
|
|
|
794
810
|
- `layerJson` over a websocket → no framing → the first chunk past the first message is misinterpreted
|
|
795
811
|
- `layerMsgPack` against a JSON-only HTTP client → garbled responses
|
|
796
|
-
- `layerNdjson` against `layerProtocolHttp` →
|
|
812
|
+
- `layerNdjson` against `layerProtocolHttp` → streams incrementally through a bounded response queue (default 16), without RPC acks
|
|
797
813
|
|
|
798
814
|
## Testing — `RpcTest.makeClient`
|
|
799
815
|
|
|
@@ -1347,6 +1363,15 @@ The generated **RPC** payload wraps the original payload as `{ entityId: string,
|
|
|
1347
1363
|
|
|
1348
1364
|
As of beta.106, `EventLogEncryption.encrypt` returns `{ iv, encryptedEntry }` for every input entry, and `EventLogMessage.WriteEntries.encryptedEntries` carries `{ entryId, iv, encryptedEntry }` values. A fresh AES-GCM IV is generated per entry. This changes the encrypted replication wire format: upgrade encrypted event-log clients and servers together rather than performing a mixed-version rolling deployment.
|
|
1349
1365
|
|
|
1366
|
+
In rc.112, `EventJournal.withRemoteUncommited` (spelling intentional) receives a
|
|
1367
|
+
non-empty readonly entry array in its callback and returns `Effect<Option<A>, ...>`.
|
|
1368
|
+
No pending entries means `None` and the callback is not invoked; `Some(result)`
|
|
1369
|
+
means it ran. Update adapters/tests that assumed every flush calls the writer.
|
|
1370
|
+
EventLog retries transient remote write failures with exponential backoff from
|
|
1371
|
+
200 ms (factor 1.5), capped at 10 seconds, so pending local entries synchronize
|
|
1372
|
+
after recovery. Preserve journal acknowledgment/idempotency semantics in custom
|
|
1373
|
+
remotes rather than adding an independent retry loop.
|
|
1374
|
+
|
|
1350
1375
|
### `WorkflowProxy` — workflow → RPC / HTTP
|
|
1351
1376
|
|
|
1352
1377
|
```ts
|
|
@@ -1372,6 +1397,11 @@ To namespace the generated rpcs, pass `prefix` as the **second** argument: `Work
|
|
|
1372
1397
|
|
|
1373
1398
|
These proxies are how you give a frontend or an external system a typed RPC/HTTP surface that drives durable workflows, without leaking workflow-engine internals.
|
|
1374
1399
|
|
|
1400
|
+
In rc.112 workflow discard endpoints return a `Schema.String` execution ID
|
|
1401
|
+
instead of `void`, for both RPC and HTTP. Call the generated `<Name>Discard` RPC
|
|
1402
|
+
normally to receive it; passing the RPC client's `{ discard: true }` option
|
|
1403
|
+
discards even that ID. Entity discard endpoints keep their separate contract.
|
|
1404
|
+
|
|
1375
1405
|
## Cluster + workflow integration — `ClusterWorkflowEngine`
|
|
1376
1406
|
|
|
1377
1407
|
The in-memory `WorkflowEngine.layerMemory` is for testing only. For production, use `ClusterWorkflowEngine.layer`, which wires the workflow engine into the cluster's `Sharding` + `MessageStorage`:
|
|
@@ -1620,4 +1650,4 @@ const usersByName = Effect.gen(function*() {
|
|
|
1620
1650
|
- For production cluster, use `NodeClusterSocket.layer` / `NodeClusterHttp.layer` (or the Bun equivalents) unless you specifically need to assemble layers manually.
|
|
1621
1651
|
- Size `maxResidentEntities` and `unprocessedMessageBatchSize` deliberately for the runner's memory and storage throughput.
|
|
1622
1652
|
- Match transport ↔ serialization: HTTP → `layerJson`; sockets/websocket/streaming → `layerNdjson` or `layerMsgPack`.
|
|
1623
|
-
- Pattern-match on `client.GetUser(...).pipe(Effect.catchTag('UserNotFound', ...), Effect.catchFilter(...))` for typed recovery; reserve broad `Effect.
|
|
1653
|
+
- Pattern-match on `client.GetUser(...).pipe(Effect.catchTag('UserNotFound', ...), Effect.catchFilter(...))` for typed recovery; reserve broad `Effect.catch` for an explicit boundary recovery policy.
|