opencode-effect-enforcer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-rpc-api
|
|
3
|
+
description: Define type-safe RPC contracts with effect/unstable/rpc — Rpc.make payload/success/error/defect schemas, RpcSchema.Stream streaming responses, RpcGroup composition (add/merge/omit/prefix/annotate), and RpcMiddleware.Service definitions shared by client and server. Use when declaring or evolving RPC procedures, building a shared contract package, adding streaming endpoints, or defining auth/observability middleware types.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Effect TypeScript expert specializing in defining shared RPC contracts with `Rpc`, `RpcGroup`, `RpcSchema`, and `RpcMiddleware` from `effect/unstable/rpc`.
|
|
7
|
+
|
|
8
|
+
This skill covers the **contract layer**: the definitions that client and server packages both import. Wiring handlers into a server is the `effect-rpc-server` skill; constructing clients and protocols is the `effect-rpc-client` skill; distributed entities are the `effect-rpc-cluster` skill.
|
|
9
|
+
|
|
10
|
+
## Effect Source Reference
|
|
11
|
+
|
|
12
|
+
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
|
+
|
|
14
|
+
Key files:
|
|
15
|
+
|
|
16
|
+
- `packages/effect/src/unstable/rpc/Rpc.ts` — `Rpc.make`, `Rpc.custom`, per-rpc combinators, `exitSchema`, `Wrapper` (`fork`/`uninterruptible`/`wrap`), `ServerClient`, every type helper (`Payload`, `Success`, `Error`, `Exit`, `ToHandlerFn`, `ResultFrom`, ...)
|
|
17
|
+
- `packages/effect/src/unstable/rpc/RpcGroup.ts` — group construction and composition (`add`/`merge`/`omit`/`prefix`/`middleware`), group vs per-rpc annotations, handler-conversion surface (`toLayer`/`toHandlers`/`toLayerHandler`/`accessHandler`/`of`)
|
|
18
|
+
- `packages/effect/src/unstable/rpc/RpcSchema.ts` — the `Stream` schema marker, `isStreamSchema`, `ClientAbort` cause annotation
|
|
19
|
+
- `packages/effect/src/unstable/rpc/RpcMiddleware.ts` — `Service` constructor, `layerClient`, `ForClient`, `ApplyServices` and the middleware function shapes
|
|
20
|
+
- `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire envelopes: `Request`, `Ack`, `Interrupt`, `Eof`, `Ping`, `ResponseChunk`, `ResponseExit`, `ExitEncoded`, `RequestId`
|
|
21
|
+
- `packages/effect/src/unstable/rpc/RpcClientError.ts` — the transport error type referenced when typing shared client aliases
|
|
22
|
+
- `packages/effect/src/unstable/rpc/index.ts` — public exports of the rpc namespace
|
|
23
|
+
- `packages/platform-node/test/fixtures/rpc-schemas.ts` — the best real-world contract fixture: rpcs, streaming, middleware, deferred responses
|
|
24
|
+
- `packages/effect/test/rpc/Rpc.test.ts` — `exitSchema`, custom defect schemas, `getStreamSchemas` semantics
|
|
25
|
+
|
|
26
|
+
## Core Model
|
|
27
|
+
|
|
28
|
+
An `Rpc` is a **value-level contract** for one procedure:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
interface Rpc<
|
|
32
|
+
Tag extends string,
|
|
33
|
+
Payload extends Schema.Top = Schema.Void,
|
|
34
|
+
Success extends Schema.Top = Schema.Void,
|
|
35
|
+
Error extends Schema.Top = Schema.Never,
|
|
36
|
+
Middleware extends RpcMiddleware.AnyService = never,
|
|
37
|
+
Requires = never
|
|
38
|
+
> // _tag, payloadSchema, successSchema, errorSchema, defectSchema,
|
|
39
|
+
// annotations: Context.Context<never>, middlewares: ReadonlySet<Middleware>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It records a tag, four schemas (payload, success, error, defect), a set of middleware service keys, and a `Context` of annotations. An `RpcGroup<R>` is an immutable `ReadonlyMap<tag, Rpc>` plus group-level annotations. Neither does any I/O — servers interpret them into handlers, clients into methods, entities into mailboxes. Define them once in a shared module and import them everywhere.
|
|
43
|
+
|
|
44
|
+
Both `Rpc` and `RpcGroup` declare `new (_: never): {}`, so both `const X = Rpc.make(...)` and `class X extends Rpc.make(...) {}` are valid (same for groups). All combinators return **new values** — these structures are immutable.
|
|
45
|
+
|
|
46
|
+
Imports used throughout (all from the `effect` package; there is no `@effect/rpc` in v4):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Context, Schema } from 'effect';
|
|
50
|
+
import { Rpc, RpcGroup, RpcMiddleware, RpcSchema } from 'effect/unstable/rpc';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Deep subpath imports also work (the package exports a `./*` wildcard), e.g. `import * as RpcSchema from 'effect/unstable/rpc/RpcSchema'` or `import { RpcClientError } from 'effect/unstable/rpc/RpcClientError'`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1. Defining RPCs — `Rpc.make`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
Rpc.make(tag, {
|
|
61
|
+
payload?: Schema.Top | Schema.Struct.Fields, // default Schema.Void
|
|
62
|
+
success?: Schema.Top, // default Schema.Void
|
|
63
|
+
error?: Schema.Top, // default Schema.Never
|
|
64
|
+
defect?: Rpc.DefectSchema, // default Schema.Defect()
|
|
65
|
+
stream?: boolean, // default false
|
|
66
|
+
primaryKey?: (payload) => string // only with struct-fields payload
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Two declaration styles, both official:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { Schema } from 'effect';
|
|
74
|
+
import { Rpc } from 'effect/unstable/rpc';
|
|
75
|
+
|
|
76
|
+
export class User extends Schema.Class<User>('User')({
|
|
77
|
+
id: Schema.String,
|
|
78
|
+
name: Schema.String
|
|
79
|
+
}) {}
|
|
80
|
+
|
|
81
|
+
export class UserNotFound extends Schema.TaggedError<UserNotFound>()('UserNotFound', {
|
|
82
|
+
id: Schema.String
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
// Style A — const value. Compact; good when listing many rpcs in one file.
|
|
86
|
+
export const Ping = Rpc.make('Ping', { success: Schema.String });
|
|
87
|
+
|
|
88
|
+
// Style B — class extends. Nominal identity; good for rpcs imported widely.
|
|
89
|
+
export class GetUser extends Rpc.make('GetUser', {
|
|
90
|
+
payload: { id: Schema.String },
|
|
91
|
+
success: User,
|
|
92
|
+
error: UserNotFound
|
|
93
|
+
}) {}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
With class style, the class itself is the rpc value (`RpcGroup.make(GetUser, ...)`) and `typeof GetUser` works with all `Rpc.*` type helpers.
|
|
97
|
+
|
|
98
|
+
### Payload: struct fields vs schema
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// Inline fields — Rpc.make builds Schema.Struct({ ... }) for you
|
|
102
|
+
Rpc.make('CreateUser', {
|
|
103
|
+
payload: { name: Schema.String, email: Schema.String },
|
|
104
|
+
success: User
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Named schema — reuse a Schema.Class (or any Schema.Top)
|
|
108
|
+
class CreateUserInput extends Schema.Class<CreateUserInput>('CreateUserInput')({
|
|
109
|
+
name: Schema.String,
|
|
110
|
+
email: Schema.String
|
|
111
|
+
}) {}
|
|
112
|
+
Rpc.make('CreateUser', { payload: CreateUserInput, success: User });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With the default `Schema.Void` payload, the generated client method takes no payload argument (`client.Ping()`).
|
|
116
|
+
|
|
117
|
+
### `defect` — controlling defect serialization
|
|
118
|
+
|
|
119
|
+
Defects (`Effect.die`) cross the wire through `defectSchema`. The default `Schema.Defect()` round-trips defects as `unknown`, JSON-encoding `Error` values as `{ name, message, cause? }` and **stripping stack traces** for security. Options:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// Keep stack traces across the wire
|
|
123
|
+
Rpc.make('Risky', { defect: Schema.Defect({ includeStack: true }) });
|
|
124
|
+
|
|
125
|
+
// Also available: { excludeCause: true } to drop Error.cause
|
|
126
|
+
// Or any schema satisfying Rpc.DefectSchema (decodes/encodes with no services):
|
|
127
|
+
Rpc.make('RiskyRaw', { defect: Schema.Any });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Note `Schema.Defect()` normalizes: a non-`Error` object like `{ message: 'boom' }` decodes back as an `Error`; non-JSON values fall back to a formatted string.
|
|
131
|
+
|
|
132
|
+
### `primaryKey` — deterministic payload identity
|
|
133
|
+
|
|
134
|
+
`primaryKey` is only available when `payload` is given as **struct fields** (the option is typed `never` for schema payloads). It makes `Rpc.make` build a `Schema.Class` for the payload that implements `PrimaryKey.symbol`, giving each payload value a deterministic string identity. The cluster layer uses it to dedupe retried sends of persisted messages:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
export const Charge = Rpc.make('Charge', {
|
|
138
|
+
payload: { invoiceId: Schema.String, amountCents: Schema.Int },
|
|
139
|
+
success: Schema.Boolean,
|
|
140
|
+
primaryKey: ({ invoiceId }) => invoiceId
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Clients still pass plain objects; the class is an implementation detail of the payload schema. See the `effect-rpc-cluster` skill for persistence semantics.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 2. Streaming Contracts — `stream: true` and `RpcSchema.Stream`
|
|
149
|
+
|
|
150
|
+
`stream: true` changes how the `success` and `error` options are interpreted:
|
|
151
|
+
|
|
152
|
+
- `successSchema` becomes `RpcSchema.Stream(success, error)` — `success` is the **element** schema, `error` is the **stream error** schema
|
|
153
|
+
- `errorSchema` is set to `Schema.Never` (the declared `error` moved into the stream)
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
export class StreamUsers extends Rpc.make('StreamUsers', {
|
|
157
|
+
payload: { since: Schema.DateTimeUtc },
|
|
158
|
+
success: User, // element schema, not the Effect success
|
|
159
|
+
error: UserNotFound, // stream error schema
|
|
160
|
+
stream: true
|
|
161
|
+
}) {}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Consequences for both sides of the contract:
|
|
165
|
+
|
|
166
|
+
- the handler must return `Stream<User, UserNotFound, R>` — or `Effect<Queue.Dequeue<User, UserNotFound | Cause.Done>, ..., R>` to drive the queue itself
|
|
167
|
+
- the client sees `Stream<User, UserNotFound | RpcClientError>` (or a `Queue.Dequeue` with `{ asQueue: true }` — see the `effect-rpc-client` skill)
|
|
168
|
+
- the rpc's terminal `Exit` success is `void`; elements travel separately as `Chunk` messages
|
|
169
|
+
|
|
170
|
+
`RpcSchema.Stream` is itself a schema, so `Rpc.make('X', { success: RpcSchema.Stream(User, UserNotFound) })` is equivalent — `stream: true` is just the ergonomic spelling. Introspect with:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
RpcSchema.isStreamSchema(StreamUsers.successSchema); // true
|
|
174
|
+
// streamSchema.success / streamSchema.error hold the element/error schemas
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `RpcSchema.ClientAbort`
|
|
178
|
+
|
|
179
|
+
`ClientAbort` is a `Cause` annotation marking interrupts that originated from a client abort: when a client interrupts a call, the server interrupts the handler fiber with it attached to the `Interrupt` reason. Servers distinguish client cancellation from shutdown by checking for `RpcSchema.ClientAbort.key` on `Interrupt` reasons — the handler-side detection snippet lives in the `effect-rpc-server` skill.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 3. Per-RPC Combinators
|
|
184
|
+
|
|
185
|
+
Every `Rpc` is `Pipeable` and exposes instance methods, each returning a new rpc:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
Rpc.make('GetUser', { payload: { id: Schema.String }, success: User })
|
|
189
|
+
.setSuccess(Schema.Option(User)) // swap success schema
|
|
190
|
+
.setError(UserNotFound) // swap error schema
|
|
191
|
+
.setPayload({ id: Schema.String, tenant: Schema.String }) // fields or Schema.Top
|
|
192
|
+
.middleware(AuthMiddleware) // attach a middleware service key
|
|
193
|
+
.prefix('users.') // tag becomes 'users.GetUser'
|
|
194
|
+
.annotate(SomeKey, value) // add one annotation
|
|
195
|
+
.annotateMerge(someContext); // merge a Context.Context<I>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Notes:
|
|
199
|
+
|
|
200
|
+
- `prefix` changes `_tag`, which is the **wire identity** of the procedure (and the key handlers are registered under). Prefix at definition time, before anything depends on the tag.
|
|
201
|
+
- `annotate`/`annotateMerge` attach metadata read by servers, clusters, and proxies (e.g. `ClusterSchema.Persisted`). Annotations are an open `Context`, so any `Context.Key` works — including your own.
|
|
202
|
+
- `middleware` accumulates into a `ReadonlySet`; attaching also threads the middleware's `provides`/`requires` through the rpc's `Requires` type parameter (section 6).
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 4. Composing Contracts — `RpcGroup`
|
|
207
|
+
|
|
208
|
+
`RpcGroup.make(...rpcs)` is variadic:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
export const UsersGroup = RpcGroup.make(GetUser, CreateUser, StreamUsers);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The rpcs are reachable at `group.requests` (`ReadonlyMap<string, Rpc>`) — useful for contract introspection tests and codegen; `Rpc.isRpc(u)` guards individual values.
|
|
215
|
+
|
|
216
|
+
### Combinators
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
UsersGroup
|
|
220
|
+
.add(DeleteUser, UpdateUser) // append rpcs (same-tag add replaces)
|
|
221
|
+
.merge(OrdersGroup, PaymentsGroup) // union of groups
|
|
222
|
+
.omit('DeleteUser', 'UpdateUser') // remove by tag (variadic)
|
|
223
|
+
.prefix('v2.') // re-tag every rpc
|
|
224
|
+
.middleware(AuthMiddleware); // attach to every rpc currently in the group
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Sharp edges, verified in source:
|
|
228
|
+
|
|
229
|
+
- `merge` is **last-wins** on duplicate rpc tags *and* duplicate group-annotation keys. No error, silent override.
|
|
230
|
+
- `middleware` and the `annotateRpcs*` methods only affect rpcs **already in the group**. Rpcs added afterwards via `.add(...)` are not covered — re-apply, or attach on the rpc itself before adding.
|
|
231
|
+
- `omit` removes the rpc from the group but does not return it; keep the original rpc value if you need it elsewhere.
|
|
232
|
+
|
|
233
|
+
### Group-level vs per-rpc annotations
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
group.annotate(SomeKey, value); // on the group itself
|
|
237
|
+
group.annotateMerge(context); // merge Context into the group
|
|
238
|
+
group.annotateRpcs(SomeKey, value); // on every rpc currently in the group
|
|
239
|
+
group.annotateRpcsMerge(context); // merge Context into every rpc
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`annotateRpcs*` does **not** override an annotation already set on an individual rpc — the implementation merges as `Context.merge(context, rpc.annotations)`, so the rpc's own value wins. Use this deliberately: set per-rpc exceptions first, then apply the group default.
|
|
243
|
+
|
|
244
|
+
### Handler-conversion surface (pointer)
|
|
245
|
+
|
|
246
|
+
`RpcGroup` also carries the methods that turn a contract into a server implementation — `toLayer(handlers | Effect<handlers>)`, `toLayerHandler(tag, handler)`, `toHandlers(handlers)`, `accessHandler(tag)`, and the identity type-checker `of(handlers)`. Their option signatures and wiring belong to the `effect-rpc-server` skill; what matters at the contract level is that the handler **shape** is fully derived from the rpc definitions (section 8), so a contract change is a compile error in every handler and client.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 5. Errors, Defects, and the Exit Schema
|
|
251
|
+
|
|
252
|
+
An rpc's effective error union is **derived**, not declared in one place:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
Rpc.Error<R> = declared error schema | every attached middleware's error schema
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
On top of that, clients add transport errors (`RpcClientError`) and any middleware `clientError` types — see the `effect-rpc-client` skill. Never re-declare a middleware's error in the rpc's `error` option; attaching the middleware adds it automatically.
|
|
259
|
+
|
|
260
|
+
Use schema error classes so errors are tagged, yieldable, and serializable:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
export class RateLimited extends Schema.TaggedError<RateLimited>()('RateLimited', {
|
|
264
|
+
retryAfterMillis: Schema.Number
|
|
265
|
+
}) {}
|
|
266
|
+
|
|
267
|
+
export class OrderError extends Schema.TaggedError<OrderError>()('OrderError', {
|
|
268
|
+
reason: Schema.Literals(['empty-cart', 'payment-failed'])
|
|
269
|
+
}) {}
|
|
270
|
+
|
|
271
|
+
const PlaceOrder = Rpc.make('PlaceOrder', {
|
|
272
|
+
payload: { cartId: Schema.String },
|
|
273
|
+
success: Schema.String,
|
|
274
|
+
error: Schema.Union([OrderError, RateLimited]) // unions take an array in v4
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `Rpc.exitSchema`
|
|
279
|
+
|
|
280
|
+
`Rpc.exitSchema(rpc)` builds the `Schema.Exit` that servers use to encode results and clients use to decode them:
|
|
281
|
+
|
|
282
|
+
- success side: the success schema — or `Schema.Void` for streaming rpcs
|
|
283
|
+
- failure side: `Schema.Union([...])` of the rpc error schema, the stream error schema (if streaming), and every middleware error schema
|
|
284
|
+
- defect side: the rpc's `defectSchema`
|
|
285
|
+
|
|
286
|
+
The result is cached per rpc value (WeakMap). Useful for serialization tests and generic envelope tooling:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { Exit } from 'effect';
|
|
290
|
+
|
|
291
|
+
const schema = Rpc.exitSchema(PlaceOrder);
|
|
292
|
+
const encoded = Schema.encodeSync(schema)(Exit.fail(new RateLimited({ retryAfterMillis: 100 })));
|
|
293
|
+
const roundTripped = Schema.decodeSync(schema)(encoded);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 6. Middleware Definition — `RpcMiddleware.Service`
|
|
299
|
+
|
|
300
|
+
A middleware is declared as a `Context.Service` class with two parameter slots:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { Context, Schema } from 'effect';
|
|
304
|
+
import { RpcMiddleware } from 'effect/unstable/rpc';
|
|
305
|
+
|
|
306
|
+
export class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
|
|
307
|
+
|
|
308
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()('Unauthorized', {}) {}
|
|
309
|
+
|
|
310
|
+
export class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
|
|
311
|
+
provides: CurrentUser; // services injected into wrapped handlers
|
|
312
|
+
requires: never; // services this middleware needs from the outer context
|
|
313
|
+
clientError: never; // error type only the client-side wrapper can produce
|
|
314
|
+
}>()('AuthMiddleware', {
|
|
315
|
+
error: Unauthorized, // wire-error schema; default Schema.Never
|
|
316
|
+
requiredForClient: true // default false
|
|
317
|
+
}) {}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Exact shape, verified in source:
|
|
321
|
+
|
|
322
|
+
- The **config type parameter** carries `{ requires?, provides?, clientError? }` — all optional, all defaulting to `never`. A bare `RpcMiddleware.Service<TimingMiddleware>()('TimingMiddleware')` is a pure observer.
|
|
323
|
+
- The **options object** has exactly two keys: `error` (a schema; becomes part of the rpc error union, section 5) and `requiredForClient` (boolean). There is no `optional`, `wrap`, `failure`, or `provides` option key — those were v3 spellings.
|
|
324
|
+
- The class statics expose `.error` and `.requiredForClient`, and the class itself is the `Context.Key` you attach with `rpc.middleware(AuthMiddleware)` / `group.middleware(AuthMiddleware)`.
|
|
325
|
+
|
|
326
|
+
### What attaching does to types
|
|
327
|
+
|
|
328
|
+
`rpc.middleware(M)` computes `Requires_new = Exclude<Requires_old, Provides<M>> | Requires<M>` (`RpcMiddleware.ApplyServices`). So:
|
|
329
|
+
|
|
330
|
+
- handlers for the rpc receive `Provides<M>` in their environment for free (`CurrentUser` above)
|
|
331
|
+
- a middleware whose `requires` names a service is satisfied by attaching a *provider middleware after it* — unfulfilled `requires` surface as extra requirements on the handlers layer
|
|
332
|
+
- `requiredForClient: true` adds `RpcMiddleware.ForClient<M>` to the client's required context, making a missing client implementation a **compile-time** error. Without it, clients silently skip middleware that has no client layer provided.
|
|
333
|
+
|
|
334
|
+
### The server-side function shape (for reference)
|
|
335
|
+
|
|
336
|
+
The service value is a function `(effect, options) => Effect` — options: `{ client, requestId, rpc, payload, headers }` (`rpc` is typed `Rpc.AnyWithProps`). It wraps handler execution: it cannot change the success value (opaque `SuccessValue`), but it can provide services, fail with the wire error, and observe the exit. Execution order: middlewares apply in attachment order with the **last attached outermost**, on both server and client. The `Layer` implementation (`Layer.succeed(AuthMiddleware)(AuthMiddleware.of(...))`) belongs to the `effect-rpc-server` skill.
|
|
337
|
+
|
|
338
|
+
### The client-side counterpart — `RpcMiddleware.layerClient`
|
|
339
|
+
|
|
340
|
+
Defined alongside the contract so client packages can provide it. The function receives `{ rpc, request, next }` and **must** call `next` (with the original or a modified request):
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { Headers } from 'effect/unstable/http';
|
|
344
|
+
|
|
345
|
+
export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
|
|
346
|
+
next({
|
|
347
|
+
...request,
|
|
348
|
+
headers: Headers.set(request.headers, 'authorization', `Bearer ${getToken()}`)
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
// Layer<RpcMiddleware.ForClient<AuthMiddleware>>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`layerClient` also accepts an `Effect` that builds the function (for middleware needing services); the layer's environment is captured and merged into every invocation. Failures here surface as the middleware's `error` type or the `clientError` type from the config.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 7. Custom Constructors — `Rpc.custom`
|
|
359
|
+
|
|
360
|
+
`Rpc.custom` builds an `Rpc.make`-alike that transforms every rpc's success/error schemas — encode a convention once (paginated lists, response envelopes) instead of repeating it per rpc:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { Schema } from 'effect';
|
|
364
|
+
import { Rpc } from 'effect/unstable/rpc';
|
|
365
|
+
|
|
366
|
+
// Type-level definition: how success/error transform
|
|
367
|
+
export interface RpcWithPagination extends Rpc.Custom {
|
|
368
|
+
readonly out: Rpc.Custom.Out<Paginated<this['success']>, this['error']>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface Paginated<S extends Schema.Top> extends
|
|
372
|
+
Schema.Struct<{
|
|
373
|
+
readonly offset: Schema.Number;
|
|
374
|
+
readonly total: Schema.Number;
|
|
375
|
+
readonly results: Schema.$Array<S>;
|
|
376
|
+
}>
|
|
377
|
+
{}
|
|
378
|
+
|
|
379
|
+
// Value-level implementation: receives { success, error, defect }, returns the transformed set
|
|
380
|
+
export const makePaginated = Rpc.custom<RpcWithPagination>((schemas) => ({
|
|
381
|
+
...schemas,
|
|
382
|
+
success: Schema.Struct({
|
|
383
|
+
offset: Schema.Number,
|
|
384
|
+
total: Schema.Number,
|
|
385
|
+
results: Schema.Array(schemas.success)
|
|
386
|
+
})
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
// Used exactly like Rpc.make — payload, stream, primaryKey all still work
|
|
390
|
+
export const ListUsers = makePaginated('listUsers', { success: User });
|
|
391
|
+
// success type: { offset: number, total: number, results: readonly User[] }
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The transformation applies to `success`/`error`/`defect` only; `payload`, `stream`, and `primaryKey` pass through unchanged. With `stream: true`, the transformed success becomes the stream **element** schema.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## 8. The Handler Contract and Wrappers
|
|
399
|
+
|
|
400
|
+
The contract fully determines the handler signature (`Rpc.ToHandlerFn`):
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
(
|
|
404
|
+
payload: Rpc.Payload<R>,
|
|
405
|
+
options: {
|
|
406
|
+
readonly client: Rpc.ServerClient; // client.id: number; client.annotate(key, value)
|
|
407
|
+
readonly requestId: RequestId;
|
|
408
|
+
readonly headers: Headers;
|
|
409
|
+
readonly rpc: R;
|
|
410
|
+
}
|
|
411
|
+
) => Rpc.WrapperOr<Rpc.ResultFrom<R, Services>>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`Rpc.ResultFrom` — what a handler may return:
|
|
415
|
+
|
|
416
|
+
- **non-stream rpc**: `Effect<Success | Deferred<Success, Error>, Error, R>`. Succeeding with a `Deferred` defers the terminal exit until the deferred completes — invisible on the wire, useful when the result arrives from another fiber/webhook later.
|
|
417
|
+
- **stream rpc**: `Stream<Element, StreamError, R>` or `Effect<Queue.Dequeue<Element, StreamError | Cause.Done>, ..., R>` (end-of-stream is `Cause.Done` in the queue's error channel).
|
|
418
|
+
|
|
419
|
+
`Scope` is always available to handlers (the server scopes each request), and services in any attached middleware's `provides` are excluded from the handler's requirements.
|
|
420
|
+
|
|
421
|
+
### Wrappers — `Rpc.fork`, `Rpc.uninterruptible`, `Rpc.wrap`
|
|
422
|
+
|
|
423
|
+
Execution-mode hints are **wrappers around the handler's return value**, not `Rpc.make` options:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
GetCount: () => Ref.get(count).pipe(Rpc.fork); // bypass server concurrency limit
|
|
427
|
+
Charge: (payload) => chargeOnce(payload).pipe(Rpc.uninterruptible); // must complete
|
|
428
|
+
Both: (payload) => work(payload).pipe(Rpc.wrap({ fork: true, uninterruptible: true }));
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`wrap` on an already-wrapped value inherits unspecified options from the existing wrapper. Introspection helpers: `Rpc.isWrapper(u)`, `Rpc.unwrap(value)`, `Rpc.wrapMap(value, f)` (maps the inner value, preserving options).
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## 9. Wire-Format Awareness — `RpcMessage`
|
|
436
|
+
|
|
437
|
+
You rarely touch `RpcMessage` directly, but the envelope explains several contract-level facts:
|
|
438
|
+
|
|
439
|
+
| Direction | Decoded messages | Purpose |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| client → server | decoded `Request`, `Ack`, `Interrupt`, `Eof`; encoded `RequestEncoded`, `AckEncoded`, `InterruptEncoded`, `Ping`, `Eof` | call, stream backpressure ack, cancellation, end-of-input, keepalive |
|
|
442
|
+
| server → client | decoded `ResponseChunk`, `ResponseExit`, `ResponseDefect`, `ClientEnd`; encoded `ResponseChunkEncoded`, `ResponseExitEncoded`, `ResponseDefectEncoded`, `Pong`, `ClientProtocolError`, `RequestEncoded` | stream elements, terminal exit, defects, lifecycle, and server-originated requests/notifications |
|
|
443
|
+
|
|
444
|
+
Key facts:
|
|
445
|
+
|
|
446
|
+
- A `RequestEncoded` carries `{ _tag: 'Request', id: string | number, tag: string, payload: unknown, headers: Array<[string, string]>, isNotification?, traceId?, spanId?, sampled? }`. The rpc's `_tag` **is the wire identity** — renaming or re-prefixing an rpc is a breaking protocol change for deployed clients.
|
|
447
|
+
- `RequestId` is a branded `string | number`; construct it with `RequestId(1)` or `RequestId('1')` (from `effect/unstable/rpc/RpcMessage`). You need it when invoking handlers manually in tests via `accessHandler`. `bigint` is not accepted.
|
|
448
|
+
- Terminal results travel as `ExitEncoded` — `Success` with a value, or `Failure` with a cause array of `Fail` (your error union, encoded), `Die` (via `defectSchema`), and `Interrupt` entries. This is exactly what `Rpc.exitSchema` encodes/decodes.
|
|
449
|
+
- Stream elements travel as batched `Chunk` messages, acknowledged by `Ack` on ack-capable protocols (sockets, workers); the HTTP protocol declares `supportsAck: false`. Incremental delivery requires a serialization with framing (e.g. ndjson) — with non-framed json over HTTP the chunks are buffered and returned in one final batch (see serialization notes in the `effect-rpc-cluster` skill).
|
|
450
|
+
- Servers represent server-originated calls and notifications with `RequestEncoded` in `FromServerEncoded`. Set `isNotification: true` for a notification; JSON-RPC serialization then omits `id`. Buffered, unframed JSON-RPC HTTP cannot deliver notifications and drops them, while framed HTTP, sockets, stdio, and workers support them.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 10. Type Helpers for Shared Contract Packages
|
|
455
|
+
|
|
456
|
+
`Rpc.*` type extractors (all take the rpc type, e.g. `typeof GetUser`):
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
Rpc.Tag<R>; // 'GetUser'
|
|
460
|
+
Rpc.Payload<R>; // decoded payload type
|
|
461
|
+
Rpc.PayloadConstructor<R>; // input accepted by payload construction
|
|
462
|
+
Rpc.Success<R>; // decoded success (Stream<...> for streaming rpcs)
|
|
463
|
+
Rpc.SuccessEncoded<R>; // encoded success
|
|
464
|
+
Rpc.SuccessChunk<R>; // stream element type (never for non-stream)
|
|
465
|
+
Rpc.Error<R>; // declared error | middleware errors (decoded)
|
|
466
|
+
Rpc.Exit<R>; // Exit<SuccessExit, ErrorExit> — stream rpcs have void success
|
|
467
|
+
Rpc.Middleware<R>; // middleware service identifiers
|
|
468
|
+
Rpc.MiddlewareClient<R>; // ForClient<...> for requiredForClient middleware
|
|
469
|
+
Rpc.Services<R>; // schema en/decoding services (both sides)
|
|
470
|
+
Rpc.ServicesClient<R>; // client-side schema services
|
|
471
|
+
Rpc.ServicesServer<R>; // server-side schema services
|
|
472
|
+
Rpc.ExtractTag<R, 'GetUser'>; // select one rpc from a union
|
|
473
|
+
Rpc.ToHandler<R>; // the Handler service type servers require
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
For generic utilities, constrain on `Rpc.Any` (tag + annotations only) or `Rpc.AnyWithProps` (all schemas/middleware visible — the type of `options.rpc` in middleware); groups use `RpcGroup.Any`. Niche helpers (`SuccessSchema`/`ErrorSchema`, `SuccessExit`/`ErrorExit`, `IsStream`, `Prefixed`, `AddError`, `AddMiddleware`, `ExtractProvides`/`ExtractRequires`/`ExcludeProvides`) also live in `Rpc.ts` — read the source when writing generic tooling.
|
|
477
|
+
|
|
478
|
+
`RpcGroup.*` helpers:
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
RpcGroup.Rpcs<typeof UsersGroup>; // union of the group's rpc definitions
|
|
482
|
+
RpcGroup.HandlersFrom<R>; // { [tag]: handler fn } object type
|
|
483
|
+
RpcGroup.HandlerFrom<R, Tag>; // one handler fn type
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The canonical use — typing a client service in the shared package without constructing anything:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import type { RpcClient } from 'effect/unstable/rpc';
|
|
490
|
+
import type { RpcClientError } from 'effect/unstable/rpc/RpcClientError';
|
|
491
|
+
|
|
492
|
+
export type UsersClient = RpcClient.RpcClient<
|
|
493
|
+
RpcGroup.Rpcs<typeof UsersGroup>,
|
|
494
|
+
RpcClientError
|
|
495
|
+
>;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Key Patterns
|
|
501
|
+
|
|
502
|
+
### A complete shared contract module
|
|
503
|
+
|
|
504
|
+
Everything client and server need, with zero runtime wiring:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// contracts/users.ts — imported by both server and client packages
|
|
508
|
+
import { Context, Schema } from 'effect';
|
|
509
|
+
import { Rpc, RpcGroup, RpcMiddleware } from 'effect/unstable/rpc';
|
|
510
|
+
|
|
511
|
+
// --- domain schemas ---
|
|
512
|
+
export class User extends Schema.Class<User>('User')({
|
|
513
|
+
id: Schema.String,
|
|
514
|
+
name: Schema.String,
|
|
515
|
+
createdAt: Schema.DateTimeUtc
|
|
516
|
+
}) {}
|
|
517
|
+
|
|
518
|
+
// --- errors ---
|
|
519
|
+
export class UserNotFound extends Schema.TaggedError<UserNotFound>()('UserNotFound', {
|
|
520
|
+
id: Schema.String
|
|
521
|
+
}) {}
|
|
522
|
+
|
|
523
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()('Unauthorized', {}) {}
|
|
524
|
+
|
|
525
|
+
// --- middleware contract ---
|
|
526
|
+
export class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
|
|
527
|
+
|
|
528
|
+
export class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
|
|
529
|
+
provides: CurrentUser;
|
|
530
|
+
}>()('AuthMiddleware', {
|
|
531
|
+
error: Unauthorized,
|
|
532
|
+
requiredForClient: true
|
|
533
|
+
}) {}
|
|
534
|
+
|
|
535
|
+
// --- procedures ---
|
|
536
|
+
export class GetUser extends Rpc.make('GetUser', {
|
|
537
|
+
payload: { id: Schema.String },
|
|
538
|
+
success: User,
|
|
539
|
+
error: UserNotFound
|
|
540
|
+
}) {}
|
|
541
|
+
|
|
542
|
+
export class CreateUser extends Rpc.make('CreateUser', {
|
|
543
|
+
payload: { name: Schema.String },
|
|
544
|
+
success: User,
|
|
545
|
+
primaryKey: ({ name }) => name // idempotent under cluster persistence
|
|
546
|
+
}) {}
|
|
547
|
+
|
|
548
|
+
export class WatchUsers extends Rpc.make('WatchUsers', {
|
|
549
|
+
payload: { since: Schema.DateTimeUtc },
|
|
550
|
+
success: User, // stream element
|
|
551
|
+
error: UserNotFound, // stream error
|
|
552
|
+
stream: true
|
|
553
|
+
}) {}
|
|
554
|
+
|
|
555
|
+
// Public group: everything requires auth
|
|
556
|
+
export const UsersGroup = RpcGroup.make(GetUser, CreateUser, WatchUsers)
|
|
557
|
+
.middleware(AuthMiddleware);
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The server package implements `UsersGroup.toLayer(...)` and a `Layer.succeed(AuthMiddleware)(...)` (see `effect-rpc-server`); the client package builds `RpcClient.make(UsersGroup)` plus `RpcMiddleware.layerClient(AuthMiddleware, ...)` (see `effect-rpc-client`).
|
|
561
|
+
|
|
562
|
+
### Versioning and namespacing with `prefix` + `merge`
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
const V1 = RpcGroup.make(GetUser, CreateUser).prefix('v1.');
|
|
566
|
+
const V2 = RpcGroup.make(GetUserV2, CreateUser, WatchUsers).prefix('v2.');
|
|
567
|
+
|
|
568
|
+
// One group served at one endpoint; tags are 'v1.GetUser', 'v2.GetUser', ...
|
|
569
|
+
export const ApiGroup = V1.merge(V2);
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Handler objects key by the **prefixed** tag (quoted keys: `'v1.GetUser': (payload) => ...`). Remember `merge` is last-wins on tag collisions — prefix before merging to make collisions impossible.
|
|
573
|
+
|
|
574
|
+
### Trimming a group for a restricted surface
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
// Internal group has admin procedures; public surface omits them
|
|
578
|
+
export const AdminGroup = RpcGroup.make(GetUser, CreateUser, DeleteUser, PurgeAll);
|
|
579
|
+
export const PublicGroup = AdminGroup.omit('DeleteUser', 'PurgeAll').middleware(AuthMiddleware);
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Marking a group for the cluster
|
|
583
|
+
|
|
584
|
+
Contracts double as entity protocols — annotate, then hand to `Entity.fromRpcGroup`:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { ClusterSchema } from 'effect/unstable/cluster';
|
|
588
|
+
|
|
589
|
+
export const DurableUsers = UsersGroup.annotateRpcs(ClusterSchema.Persisted, true);
|
|
590
|
+
// Entity.fromRpcGroup('Users', DurableUsers) — see the effect-rpc-cluster skill
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Exit-schema round-trip test for a contract
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
import { assert, it } from '@effect/vitest';
|
|
597
|
+
import { Exit, Schema } from 'effect';
|
|
598
|
+
import { Rpc } from 'effect/unstable/rpc';
|
|
599
|
+
|
|
600
|
+
it('GetUser exits round-trip', () => {
|
|
601
|
+
const schema = Rpc.exitSchema(GetUser);
|
|
602
|
+
const exit = Exit.fail(new UserNotFound({ id: 'u1' }));
|
|
603
|
+
const decoded = Schema.decodeSync(schema)(Schema.encodeSync(schema)(exit));
|
|
604
|
+
assert(Exit.isFailure(decoded));
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Common Mistakes
|
|
609
|
+
|
|
610
|
+
1. **Importing from `@effect/rpc`.** The package does not exist in v4 — everything is `effect/unstable/rpc` (deep subpaths like `effect/unstable/rpc/RpcMessage` also work).
|
|
611
|
+
2. **Reaching for `Rpc.fromTaggedRequest` / `Schema.TaggedRequest`.** Neither exists in v4. The tag is `Rpc.make`'s first argument; the payload schema carries no `_tag` field — the wire envelope transports the tag separately.
|
|
612
|
+
3. **Expecting `error` to stay the Effect error with `stream: true`.** It becomes the *stream* error schema and the rpc's `errorSchema` is set to `Schema.Never`. Likewise `success` becomes the *element* schema.
|
|
613
|
+
4. **Passing `primaryKey` with a schema payload.** It is typed `never` unless `payload` is inline struct fields — wrap the fields inline or drop `primaryKey`.
|
|
614
|
+
5. **Using v3 middleware option keys.** `RpcMiddleware.Service` options are exactly `{ error?, requiredForClient? }`; `provides`/`requires`/`clientError` go in the second *type parameter*, and `optional`/`wrap`/`failure` do not exist (v4 middleware always wraps).
|
|
615
|
+
6. **Re-declaring a middleware's error in the rpc's `error` option.** Attaching the middleware already unions its `error` schema into `Rpc.Error` and `exitSchema` — declaring it twice bloats the wire union.
|
|
616
|
+
7. **Assuming `group.middleware(...)` / `annotateRpcs(...)` cover later additions.** They snapshot the rpcs currently in the group; rpcs added afterwards via `.add(...)` are unaffected.
|
|
617
|
+
8. **Expecting `annotateRpcs` to override per-rpc annotations.** Per-rpc values win (`Context.merge(context, rpc.annotations)`); the group call only fills in missing keys.
|
|
618
|
+
9. **Relying on `merge` to detect tag collisions.** It is silent last-wins for both rpc tags and group annotation keys — `prefix` before merging.
|
|
619
|
+
10. **Treating `Rpc.fork`/`Rpc.uninterruptible` as `Rpc.make` options.** They are wrappers applied to a handler's *return value*: `effect.pipe(Rpc.fork)`.
|
|
620
|
+
11. **Mutating in place.** `annotate`, `prefix`, `middleware`, `setSuccess`, etc. all return new `Rpc`/`RpcGroup` values; discarding the return value is a no-op.
|
|
621
|
+
12. **Renaming or re-prefixing rpcs after deployment.** `_tag` is the wire identity; old clients will send tags the server no longer knows. Treat tag changes like breaking schema changes.
|
|
622
|
+
13. **Passing a union to `Schema.Union` variadically.** v4 takes an array: `Schema.Union([OrderError, RateLimited])`, not `Schema.Union(OrderError, RateLimited)`.
|
|
623
|
+
14. **Declaring errors as plain `Schema.Struct`s.** Use `Schema.TaggedError` (or `Schema.Error` with a `Schema.tag` field) so errors are yieldable, `catchTag`-able, and carry a stable `_tag` on the wire. The former `Schema.TaggedErrorClass` / `Schema.ErrorClass` names were removed in beta.104.
|
|
624
|
+
15. **Expecting stack traces in remote defects.** The default `Schema.Defect()` strips stacks; opt in per rpc with `defect: Schema.Defect({ includeStack: true })`.
|