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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,488 @@
1
+ ---
2
+ name: effect-atom-rpc
3
+ description: Build reactive Atom-aware RPC clients for React/Atom UIs using AtomRpc. Provides cached query atoms, invalidating mutation atoms, SSR hydration, and reactivity-key invalidation on top of an RpcGroup.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in `effect/unstable/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
7
+
8
+ ## When to use this skill
9
+
10
+ `AtomRpc` sits on top of an `RpcGroup` and gives you:
11
+
12
+ - A `Service`-style RPC client whose dependencies are wired through an `AtomRuntime`
13
+ - `client.query(tag, payload, options?)` — a cached, reactive `Atom<AsyncResult<...>>`
14
+ - `client.mutation(tag)` — an `AtomResultFn` that runs the rpc and invalidates dependent atoms
15
+ - SSR hydration via `Atom.serializable` when you pass `serializationKey`
16
+ - Time-to-live caching, reactivity-key invalidation, and stream rpcs surfaced as `PullResult` atoms
17
+
18
+ Use this skill when building React (or Atom-based) frontends that consume an existing `RpcGroup`.
19
+
20
+ For the underlying RPC definitions, see the `effect-rpc-cluster` skill.
21
+ For Atom fundamentals (`Atom.make`, `family`, `keepAlive`, `AsyncResult`, hydration), see the `effect-atom-state` and `effect-react-vm` skills.
22
+
23
+ ## Effect Source Reference
24
+
25
+ - `packages/effect/src/unstable/reactivity/AtomRpc.ts` — the whole API (~270 lines)
26
+ - `packages/effect/test/reactivity/AtomRpc.test.ts` — minimal usage + serialization test
27
+ - `packages/effect/src/unstable/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
28
+ - `packages/effect/src/unstable/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
29
+ - `packages/effect/src/unstable/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
30
+ - `packages/effect/src/unstable/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
31
+ - `packages/effect/src/unstable/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
32
+
33
+ ## Imports
34
+
35
+ ```ts
36
+ import {
37
+ AsyncResult,
38
+ Atom,
39
+ AtomRpc,
40
+ Hydration,
41
+ Reactivity
42
+ } from 'effect/unstable/reactivity';
43
+ import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
44
+ import { FetchHttpClient } from 'effect/unstable/http';
45
+ import { useAtomValue, useAtomSet } from '@effect/atom-react';
46
+ ```
47
+
48
+ ## Core idea
49
+
50
+ `AtomRpc.Service<Self>()(id, options)` produces a `Context.Service` subclass whose service value is an `RpcClient.RpcClient.Flat<Rpcs, RpcClientError>` (the `flatten: true` form of the RPC client — single function `(tag, payload, options?)`). It also attaches three things to the class itself:
51
+
52
+ ```ts
53
+ class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', { ... }) {
54
+ // inherited from AtomRpc.Service:
55
+ static readonly runtime: Atom.AtomRuntime<UsersClient>;
56
+ static query: (tag, payload, options?) => Atom<AsyncResult<...>> | Atom.Writable<PullResult<...>, void>;
57
+ static mutation: (tag) => Atom.AtomResultFn<{ payload, headers?, reactivityKeys? }, ..., ...>;
58
+ }
59
+ ```
60
+
61
+ The `runtime` is what powers `query` and `mutation` — internally it builds a `Layer` that runs the RPC client effect and threads the protocol layer in. You don't usually touch `runtime` directly; React components use `query` / `mutation` via Atom hooks.
62
+
63
+ ## Defining an AtomRpc client
64
+
65
+ ```ts
66
+ import { Layer } from 'effect';
67
+ import { AtomRpc } from 'effect/unstable/reactivity';
68
+ import { FetchHttpClient } from 'effect/unstable/http';
69
+ import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
70
+ import { UsersGroup } from '../shared/users-rpc.ts';
71
+
72
+ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
73
+ group: UsersGroup,
74
+ protocol: RpcClient.layerProtocolHttp({ url: '/api/rpc' }).pipe(
75
+ Layer.provide(RpcSerialization.layerJson),
76
+ Layer.provide(FetchHttpClient.layer)
77
+ )
78
+ }) {}
79
+ ```
80
+
81
+ Required: `group` and `protocol`.
82
+
83
+ `protocol` accepts either a `Layer` or a function `(get: AtomContext) => Layer`. The function form lets the protocol layer read other atoms — for example, take the current auth token from a `tokenAtom` and bake it into the HTTP client:
84
+
85
+ ```ts
86
+ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
87
+ group: UsersGroup,
88
+ protocol: (get) =>
89
+ Layer.unwrap(
90
+ Effect.gen(function*() {
91
+ const token = get(tokenAtom);
92
+ return RpcClient.layerProtocolHttp({
93
+ url: '/api/rpc',
94
+ transformClient: HttpClient.mapRequest(
95
+ HttpClientRequest.setHeader('authorization', `Bearer ${token}`)
96
+ )
97
+ });
98
+ })
99
+ )
100
+ }) {}
101
+ ```
102
+
103
+ Optional config:
104
+
105
+ ```ts
106
+ AtomRpc.Service<UsersClient>()('UsersClient', {
107
+ group: UsersGroup,
108
+ protocol: ProtocolLayer,
109
+ spanPrefix: 'UsersClient',
110
+ spanAttributes: { app: 'admin-ui' },
111
+ disableTracing: false,
112
+ generateRequestId: customRequestIdFactory, // for deterministic IDs in tests
113
+ makeEffect: undefined, // override the default RpcClient.make
114
+ runtime: Atom.runtime // override the runtime factory (e.g., Atom.runtime.withReactivity)
115
+ });
116
+ ```
117
+
118
+ `makeEffect` is mainly for testing — instead of building a real `RpcClient` from `protocol`, supply an Effect that returns the flat client directly. The test suite uses this to short-circuit network for unit tests:
119
+
120
+ ```ts
121
+ makeEffect: Effect.succeed(
122
+ ((tag, payload) => {
123
+ if (tag === 'GetUser') return Effect.succeed({ id: payload.id, name: `user-${payload.id}` });
124
+ return Effect.die(`unexpected tag: ${tag}`);
125
+ }) as any
126
+ );
127
+ ```
128
+
129
+ ## `query` — cached reactive reads
130
+
131
+ ```ts
132
+ client.query(tag, payload, options?) =>
133
+ non-stream → Atom<AsyncResult<Success, Error | RpcClientError | MiddlewareError>>
134
+ stream → Atom.Writable<PullResult<Success, Error | RpcClientError | ...>, void>
135
+ ```
136
+
137
+ Query atoms are **cached by request key** — calling `query('GetUser', { id: '1' })` from many components returns the *same* atom, so the rpc fires once and shares the result.
138
+
139
+ Effect v4 query wrappers preserve serialization and retention metadata while adding reactivity. It is therefore safe to combine `reactivityKeys`, `serializationKey`, and `timeToLive`; hydration identity and idle retention are not discarded by the reactive wrapper.
140
+
141
+ ```ts
142
+ import { useAtomValue } from '@effect/atom-react';
143
+
144
+ function UserProfile({ id }: { id: string }) {
145
+ const result = useAtomValue(
146
+ UsersClient.query('GetUser', { id }, {
147
+ timeToLive: '30 seconds',
148
+ serializationKey: `user-${id}`,
149
+ reactivityKeys: ['users', `user-${id}`]
150
+ })
151
+ );
152
+
153
+ return AsyncResult.builder(result)
154
+ .onWaiting(() => <Spinner />)
155
+ .onError((error) => <ErrorView error={error} />)
156
+ .onSuccess((user) => <UserCard user={user} />)
157
+ .render();
158
+ }
159
+ ```
160
+
161
+ ### Query options
162
+
163
+ ```ts
164
+ client.query(tag, payload, {
165
+ headers?: Headers.Input, // per-call headers
166
+ reactivityKeys?: ReadonlyArray<unknown> // invalidation keys
167
+ | ReadonlyRecord<string, ReadonlyArray<unknown>>,
168
+ timeToLive?: Duration.Input, // cache lifetime; idle eviction
169
+ serializationKey?: string // makes the atom hydration-friendly
170
+ });
171
+ ```
172
+
173
+ - **`reactivityKeys`** — keys this atom listens to. When a `mutation` (or any `Reactivity.invalidate`) fires with overlapping keys, this atom re-fetches.
174
+ - **`timeToLive`** — `Duration.Input | "infinity"`. With a finite duration, the atom is removed from the cache after that long without subscribers (`Atom.setIdleTTL`). With `Duration.infinity`, the atom is `Atom.keepAlive`'d (never evicted). Without `timeToLive`, the atom resets when the last subscriber unmounts.
175
+ - **`serializationKey`** — for non-stream queries, a stable key opts the atom into `Atom.serializable` keyed by `AtomRpc:${tag}:${serializationKey}`. Without a `serializationKey`, the query atom is cached for the current registry but is not serializable/hydratable, so SSR clients will fetch again. Stream rpcs cannot be serializable.
176
+
177
+ ### Stream rpcs
178
+
179
+ When the rpc has `stream: true`, `query` returns an `Atom.Writable<PullResult<A, E>, void>` instead of `Atom<AsyncResult<A, E>>`:
180
+
181
+ ```tsx
182
+ const messageAtom = ChatClient.query('Subscribe', { roomId: 'r1' });
183
+
184
+ function Messages() {
185
+ const result = useAtomValue(messageAtom);
186
+ const pullNext = useAtomSet(messageAtom);
187
+
188
+ return AsyncResult.matchWithWaiting(result, {
189
+ onWaiting: () => <Spinner />,
190
+ onError: (error) => <ErrorView error={error} />,
191
+ onDefect: (defect) => <ErrorView error={defect} />,
192
+ onSuccess: (success) => (
193
+ <MessageList
194
+ items={success.value.items}
195
+ done={success.value.done}
196
+ onLoadMore={() => pullNext()}
197
+ />
198
+ )
199
+ });
200
+ }
201
+ ```
202
+
203
+ `PullResult<A, E>` is an `AsyncResult<{ done: boolean; items: NonEmptyArray<A> }, E | NoSuchElementError>`. The `waiting` flag is top-level on the `AsyncResult`; `items` and `done` live under the success `value`. Use `Atom.runtime.pull` semantics — write `void` to request the next chunk.
204
+
205
+ ## `mutation` — invalidating writes
206
+
207
+ ```ts
208
+ client.mutation(tag) => Atom.AtomResultFn<
209
+ { payload, headers?, reactivityKeys? },
210
+ Success,
211
+ Error | RpcClientError | MiddlewareError
212
+ >;
213
+ ```
214
+
215
+ ```ts
216
+ import { useAtomSet } from '@effect/atom-react';
217
+
218
+ function CreateUserButton() {
219
+ const createUser = useAtomSet(UsersClient.mutation('CreateUser'));
220
+
221
+ return (
222
+ <button
223
+ onClick={() =>
224
+ createUser({
225
+ payload: { name: 'alice', email: 'alice@example.com' },
226
+ reactivityKeys: ['users'] // invalidates every query atom keyed 'users'
227
+ })}
228
+ >
229
+ Create
230
+ </button>
231
+ );
232
+ }
233
+ ```
234
+
235
+ Calling the returned function:
236
+
237
+ - Runs the rpc with the given `payload` and optional `headers`
238
+ - After success, fires `Reactivity.invalidate(reactivityKeys)`, causing matching `query` atoms to re-fetch
239
+ - Returns nothing (the result is reflected via the mutation atom's own `AsyncResult` state)
240
+
241
+ The mutation atom itself is also `Atom.serializable` keyed `AtomRpc:mutation:${tag}` — useful when you want to inspect the most recent mutation result in the UI (loading state, last error).
242
+
243
+ ### Mutation options
244
+
245
+ ```ts
246
+ mutation({
247
+ payload: Rpc.PayloadConstructor<Rpc>, // required; constructed via the rpc payload schema
248
+ headers?: Headers.Input, // per-call headers
249
+ reactivityKeys?: ReadonlyArray<unknown> // keys to invalidate after success
250
+ | ReadonlyRecord<string, ReadonlyArray<unknown>>
251
+ });
252
+ ```
253
+
254
+ Stream rpcs are **not** valid mutation targets. Use `query` for streams.
255
+
256
+ ## Reactivity keys
257
+
258
+ Two equivalent ways to declare keys:
259
+
260
+ ```ts
261
+ // Plain array → all keys map to the global namespace
262
+ reactivityKeys: ['users', 'user-1'];
263
+
264
+ // Record → keys are scoped to namespaces (useful for cross-cutting domains)
265
+ reactivityKeys: {
266
+ users: ['user-1', 'user-2'],
267
+ tenants: ['t1']
268
+ }
269
+ ```
270
+
271
+ A `query` re-fires when *any* of its declared keys appears in an invalidation event. The matching is structural — if two atoms both list `'user-1'`, mutating with `reactivityKeys: ['user-1']` invalidates both.
272
+
273
+ For more advanced invalidation patterns (e.g., conditional invalidation, debounced invalidation), use `Reactivity.invalidate(keys)` and `Reactivity.mutation(effect, keys)` directly inside Atom-runtime effects.
274
+
275
+ ## SSR hydration
276
+
277
+ Hydration uses `Atom.serializable` (which `query` opts into when you pass `serializationKey`):
278
+
279
+ ```ts
280
+ // --- server: render then dehydrate ---
281
+ import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
282
+
283
+ const registry = AtomRegistry.make();
284
+ const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
285
+ serializationKey: 'u1'
286
+ });
287
+
288
+ const unmount = registry.mount(userAtom);
289
+ yield* Effect.yieldNow;
290
+ yield* Effect.yieldNow;
291
+
292
+ const dehydrated = Hydration.toValues(Hydration.dehydrate(registry));
293
+ unmount();
294
+ // dehydrated is a JSON-safe array of { key, value } pairs to inline in the HTML
295
+ ```
296
+
297
+ ```tsx
298
+ // --- client: hydrate before rendering ---
299
+ import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
300
+ import { RegistryContext } from '@effect/atom-react';
301
+
302
+ const registry = AtomRegistry.make();
303
+ Hydration.hydrate(registry, dehydratedFromServer);
304
+
305
+ <RegistryContext.Provider value={registry}>
306
+ <App />
307
+ </RegistryContext.Provider>;
308
+ ```
309
+
310
+ Stream rpcs are not serializable — only non-stream `query` atoms with a stable `serializationKey` can be dehydrated and hydrated.
311
+
312
+ ## Custom runtime
313
+
314
+ `AtomRpc.Service` accepts a `runtime` factory option. The default is `Atom.runtime`. Override it when you want all the atoms produced by this client to share a custom reactivity scope, registry, or annotation:
315
+
316
+ ```ts
317
+ const myRuntime = Atom.runtime.withReactivity(['shared-namespace']);
318
+
319
+ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
320
+ group: UsersGroup,
321
+ protocol: ProtocolLayer,
322
+ runtime: myRuntime
323
+ }) {}
324
+ ```
325
+
326
+ ## End-to-end example
327
+
328
+ ```ts
329
+ // --- shared/users-rpc.ts ---
330
+ import { Schema } from 'effect';
331
+ import { Rpc, RpcGroup } from 'effect/unstable/rpc';
332
+
333
+ export class User extends Schema.Class<User>('User')({
334
+ id: Schema.String,
335
+ name: Schema.String,
336
+ email: Schema.String
337
+ }) {}
338
+
339
+ export class UserNotFound extends Schema.Error<UserNotFound>('UserNotFound')({
340
+ _tag: Schema.tag('UserNotFound'),
341
+ id: Schema.String
342
+ }) {}
343
+
344
+ export const GetUser = Rpc.make('GetUser', {
345
+ payload: { id: Schema.String },
346
+ success: User,
347
+ error: UserNotFound
348
+ });
349
+
350
+ export const ListUsers = Rpc.make('ListUsers', {
351
+ success: Schema.Array(User)
352
+ });
353
+
354
+ export const CreateUser = Rpc.make('CreateUser', {
355
+ payload: { name: Schema.String, email: Schema.String },
356
+ success: User
357
+ });
358
+
359
+ export const UsersGroup = RpcGroup.make(GetUser, ListUsers, CreateUser);
360
+ ```
361
+
362
+ ```ts
363
+ // --- frontend/clients/users-client.ts ---
364
+ import { Layer } from 'effect';
365
+ import { FetchHttpClient } from 'effect/unstable/http';
366
+ import { AtomRpc } from 'effect/unstable/reactivity';
367
+ import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
368
+ import { UsersGroup } from '../../shared/users-rpc.ts';
369
+
370
+ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
371
+ group: UsersGroup,
372
+ protocol: RpcClient.layerProtocolHttp({ url: '/api/rpc' }).pipe(
373
+ Layer.provide(RpcSerialization.layerJson),
374
+ Layer.provide(FetchHttpClient.layer)
375
+ ),
376
+ spanPrefix: 'UsersClient'
377
+ }) {}
378
+ ```
379
+
380
+ ```tsx
381
+ // --- frontend/components/UserList.tsx ---
382
+ import { useAtomValue, useAtomSet } from '@effect/atom-react';
383
+ import { AsyncResult } from 'effect/unstable/reactivity';
384
+ import { UsersClient } from '../clients/users-client';
385
+
386
+ export function UserList() {
387
+ const users = useAtomValue(
388
+ UsersClient.query('ListUsers', undefined, {
389
+ timeToLive: '1 minute',
390
+ reactivityKeys: ['users'],
391
+ serializationKey: 'all'
392
+ })
393
+ );
394
+
395
+ const createUser = useAtomSet(UsersClient.mutation('CreateUser'));
396
+
397
+ const onAdd = () =>
398
+ createUser({
399
+ payload: { name: 'New user', email: 'new@example.com' },
400
+ reactivityKeys: ['users']
401
+ });
402
+
403
+ return (
404
+ <>
405
+ <button onClick={onAdd}>Add user</button>
406
+ {AsyncResult.builder(users)
407
+ .onWaiting(() => <p>Loading…</p>)
408
+ .onError((err) => <p>Error: {String(err)}</p>)
409
+ .onSuccess((list) => (
410
+ <ul>
411
+ {list.map((u) => <li key={u.id}>{u.name}</li>)}
412
+ </ul>
413
+ ))
414
+ .render()}
415
+ </>
416
+ );
417
+ }
418
+ ```
419
+
420
+ ```tsx
421
+ // --- frontend/components/UserDetail.tsx ---
422
+ export function UserDetail({ id }: { id: string }) {
423
+ const user = useAtomValue(
424
+ UsersClient.query('GetUser', { id }, {
425
+ timeToLive: '30 seconds',
426
+ reactivityKeys: ['users', `user-${id}`],
427
+ serializationKey: `user-${id}`
428
+ })
429
+ );
430
+
431
+ return AsyncResult.builder(user)
432
+ .onWaiting(() => <Spinner />)
433
+ .onErrorTag('UserNotFound', (err) => <NotFound id={err.id} />)
434
+ .onError((err) => <ErrorView error={err} />)
435
+ .onSuccess((u) => <UserCard user={u} />)
436
+ .render();
437
+ }
438
+ ```
439
+
440
+ ## Sibling: `AtomHttpApi`
441
+
442
+ If your backend exposes an `HttpApi` instead of an `RpcGroup`, `AtomHttpApi.Service<Self>()(id, options)` is the same idea applied to `HttpApiGroup`/`HttpApiEndpoint`:
443
+
444
+ ```ts
445
+ import { AtomHttpApi } from 'effect/unstable/reactivity';
446
+
447
+ class ApiClient extends AtomHttpApi.Service<ApiClient>()('ApiClient', {
448
+ api: MyHttpApi,
449
+ httpClient: FetchHttpClient.layer,
450
+ baseUrl: '/api'
451
+ }) {}
452
+
453
+ ApiClient.query('users', 'getById', {
454
+ params: { id: 'u1' },
455
+ timeToLive: '30 seconds',
456
+ reactivityKeys: ['users', 'user-u1'],
457
+ serializationKey: 'user-u1'
458
+ });
459
+
460
+ const updateUser = useAtomSet(ApiClient.mutation('users', 'update'));
461
+ ```
462
+
463
+ The `query`/`mutation` arguments differ (you pass `groupName, endpointName, request` because HttpApi has groups). Non-stream `AtomHttpApi.query` atoms are serializable only in decoded-only mode **and** when you provide a stable `serializationKey`; query hydration keys use `AtomHttpApi:${group}:${endpoint}:${serializationKey}`. Decoded-only AtomHttpApi query/mutation serialization uses endpoint + endpoint-middleware wire error schemas; do not document client-only middleware errors as serialized failures.
464
+
465
+ `AtomHttpApi` error types follow the `HttpApiClient` endpoint shape: endpoint decoded errors, endpoint middleware errors, and client middleware errors. With `responseMode: 'response-only'`, endpoint decoded errors are excluded because the caller receives the raw response, but endpoint middleware and client middleware errors remain typed failures. Low-level `HttpClientError` and `SchemaError` are raised as defects by the AtomHttpApi runtime.
466
+
467
+ ## Anti-patterns
468
+
469
+ 1. **Re-creating the client on every render.** `AtomRpc.Service` produces a *singleton* class — define it once at module scope and import. Constructing it inside a component creates a new runtime per render.
470
+ 2. **Forgetting `serializationKey` for hydration.** Without it, SSR-rendered atoms re-fetch on the client. Add `serializationKey` to every query you want to hydrate.
471
+ 3. **Using `query` for an action.** Queries are cached and may run more than once due to subscription churn. Use `mutation` for any side-effectful or state-changing call.
472
+ 4. **Forgetting `reactivityKeys` on mutations.** A mutation that doesn't list keys won't invalidate any queries — the UI will look stale until the user refreshes.
473
+ 5. **Trying to make a stream rpc serializable.** Not supported — `serializationKey` is silently ignored on streams.
474
+ 6. **Overlapping reactivity keys without intent.** A widely-used key like `['users']` invalidates *every* user query. Scope keys narrowly (`['user-${id}']`) when only one entry changes; use the broad key only when a list-level refresh is intended.
475
+ 7. **Assuming every error has `_tag`.** The failure channel includes declared RPC errors, RPC middleware wire errors, and `RpcClientError` transport faults. When you model errors as tagged schemas, use `.onErrorTag(...)` or check `_tag`; otherwise handle by schema/predicate with `.onError(...)` / `.onErrorIf(...)`.
476
+ 8. **Mixing `Atom.runtime` factories.** If you specify a custom `runtime` for `AtomRpc.Service`, use it consistently across related queries; mixing runtimes scopes reactivity differently and breaks invalidation.
477
+
478
+ ## Rules
479
+
480
+ - Define the `RpcGroup` in a shared module; the same `UsersGroup` powers `RpcServer`, `RpcClient`, and `AtomRpc.Service`.
481
+ - One `AtomRpc.Service` class per logical client; module-singleton, never per-component.
482
+ - Always pass `reactivityKeys` to mutations so dependent queries refresh.
483
+ - Pass `serializationKey` to every query you want SSR-hydratable.
484
+ - Pick `timeToLive` deliberately: short (`'10 seconds'`) for hot data, long (`Duration.infinity`) for reference data, omit for "tear down on unmount".
485
+ - Use `AsyncResult.builder(...).onWaiting(...).onError(...).onSuccess(...)` (or `AsyncResult.matchWithWaiting`) to render — never check `.waiting` and `.error` ad-hoc. Reserve `.onFailure((cause) => ...)` for whole-`Cause` fallbacks after typed error branches.
486
+ - For protocol layers that depend on auth tokens or other reactive state, use the `(get) => Layer` form of `protocol` so the client rebuilds when those atoms change.
487
+ - For typed error recovery, handle declared RPC errors, RPC middleware wire errors, and `RpcClientError`; branch on `_tag` only when the relevant errors are tagged.
488
+ - Cross-link to `effect-atom-state` and `effect-react-vm` skills for atom-side patterns; this skill covers only the RPC bridge.