opencode-effect-enforcer 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +15 -305
- package/guidance/progressive-disclosure-guidance.md +18 -24
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +2 -2
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +4 -4
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
- package/src/guidance.ts +0 -1
|
@@ -9,7 +9,11 @@ Use this skill when implementing tools for AI language models using the Effect A
|
|
|
9
9
|
|
|
10
10
|
## Effect AI Documentation Access
|
|
11
11
|
|
|
12
|
-
For
|
|
12
|
+
For core AI APIs inspect `packages/effect/src/ai/` at `effect@4.0.0` in the
|
|
13
|
+
Effect source reference; provider implementations live under `packages/ai/`.
|
|
14
|
+
Keep all Effect-family packages on the same version. These APIs are tagged
|
|
15
|
+
`@stability unstable`, so minor releases may break them despite stable-looking
|
|
16
|
+
import paths.
|
|
13
17
|
|
|
14
18
|
Reference this for:
|
|
15
19
|
|
|
@@ -83,7 +87,7 @@ Use this pattern when the surrounding framework wants an async callback surface
|
|
|
83
87
|
### Basic Tool Definition
|
|
84
88
|
|
|
85
89
|
```typescript
|
|
86
|
-
import * as Tool from 'effect/
|
|
90
|
+
import * as Tool from 'effect/ai/Tool';
|
|
87
91
|
import * as Schema from 'effect/Schema';
|
|
88
92
|
|
|
89
93
|
const GetCurrentTime = Tool.make('GetCurrentTime', {
|
|
@@ -107,7 +111,7 @@ type Result = Tool.Success<typeof GetCurrentTime>;
|
|
|
107
111
|
If you have an existing domain schema you want to use as a tool, create the tool with `Tool.make` and reference the schema directly:
|
|
108
112
|
|
|
109
113
|
```typescript
|
|
110
|
-
import * as Tool from 'effect/
|
|
114
|
+
import * as Tool from 'effect/ai/Tool';
|
|
111
115
|
import * as Schema from 'effect/Schema';
|
|
112
116
|
|
|
113
117
|
const UserResult = Schema.Struct({
|
|
@@ -139,7 +143,7 @@ type Success = Tool.Success<typeof GetUserTool>;
|
|
|
139
143
|
### Tool with Parameters
|
|
140
144
|
|
|
141
145
|
```typescript
|
|
142
|
-
import * as Tool from 'effect/
|
|
146
|
+
import * as Tool from 'effect/ai/Tool';
|
|
143
147
|
import { Schema } from 'effect';
|
|
144
148
|
|
|
145
149
|
const GetWeather = Tool.make('GetWeather', {
|
|
@@ -169,7 +173,7 @@ type Success = Tool.Success<typeof GetWeather>;
|
|
|
169
173
|
### Tool with Failure Handling
|
|
170
174
|
|
|
171
175
|
```typescript
|
|
172
|
-
import * as Tool from 'effect/
|
|
176
|
+
import * as Tool from 'effect/ai/Tool';
|
|
173
177
|
import { Schema } from 'effect';
|
|
174
178
|
|
|
175
179
|
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
@@ -196,10 +200,7 @@ const FindUser = Tool.make('FindUser', {
|
|
|
196
200
|
name: Schema.String,
|
|
197
201
|
email: Schema.String
|
|
198
202
|
}),
|
|
199
|
-
failure: Schema.Union([
|
|
200
|
-
Schema.instanceOf(UserNotFound),
|
|
201
|
-
Schema.instanceOf(DatabaseError)
|
|
202
|
-
]),
|
|
203
|
+
failure: Schema.Union([UserNotFound, DatabaseError]),
|
|
203
204
|
failureMode: 'error'
|
|
204
205
|
});
|
|
205
206
|
|
|
@@ -222,12 +223,14 @@ annotation and `Tool.FailureOrigin` type. Keep validation, declared handler, and
|
|
|
222
223
|
internal failures distinct when presenting or reporting them.
|
|
223
224
|
|
|
224
225
|
- `"error"` (default): Failures go to Effect error channel
|
|
225
|
-
- `"return"`:
|
|
226
|
+
- `"return"`: Parameter and handler failures become failed tool results. Result
|
|
227
|
+
encoding can still fail the returned Stream with `AiError`; return mode does
|
|
228
|
+
not make stream consumption infallible.
|
|
226
229
|
|
|
227
230
|
### Tool with Service Dependencies
|
|
228
231
|
|
|
229
232
|
```typescript
|
|
230
|
-
import * as Tool from 'effect/
|
|
233
|
+
import * as Tool from 'effect/ai/Tool';
|
|
231
234
|
import * as Context from 'effect/Context';
|
|
232
235
|
import { Schema } from 'effect';
|
|
233
236
|
|
|
@@ -261,8 +264,8 @@ type Requirements = Tool.HandlerServices<typeof QueryDatabase>;
|
|
|
261
264
|
### Basic Toolkit
|
|
262
265
|
|
|
263
266
|
```typescript
|
|
264
|
-
import * as Toolkit from 'effect/
|
|
265
|
-
import * as Tool from 'effect/
|
|
267
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
268
|
+
import * as Tool from 'effect/ai/Tool';
|
|
266
269
|
import { Effect, Schema } from 'effect';
|
|
267
270
|
|
|
268
271
|
const GetCurrentTime = Tool.make('GetCurrentTime', {
|
|
@@ -473,7 +476,7 @@ Keep static descriptions on the tool for invariant behavior. Put runtime-specifi
|
|
|
473
476
|
### Merging Toolkits
|
|
474
477
|
|
|
475
478
|
```typescript
|
|
476
|
-
import * as Toolkit from 'effect/
|
|
479
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
477
480
|
|
|
478
481
|
const mathToolkit = Toolkit.make(
|
|
479
482
|
Tool.make('add', {
|
|
@@ -510,7 +513,7 @@ type AllTools = Toolkit.Tools<typeof combined>;
|
|
|
510
513
|
### Basic Provider Tool
|
|
511
514
|
|
|
512
515
|
```typescript
|
|
513
|
-
import * as Tool from 'effect/
|
|
516
|
+
import * as Tool from 'effect/ai/Tool';
|
|
514
517
|
import { Schema } from 'effect';
|
|
515
518
|
|
|
516
519
|
const AnthropicBash = Tool.providerDefined({
|
|
@@ -557,7 +560,7 @@ const nativeTools = Toolkit.make(
|
|
|
557
560
|
### Provider Tool with Handler
|
|
558
561
|
|
|
559
562
|
```typescript
|
|
560
|
-
import * as Tool from 'effect/
|
|
563
|
+
import * as Tool from 'effect/ai/Tool';
|
|
561
564
|
import { Schema } from 'effect';
|
|
562
565
|
|
|
563
566
|
const WebSearch = Tool.providerDefined({
|
|
@@ -614,7 +617,7 @@ declare const performSearch: (query: string) => Effect.Effect<
|
|
|
614
617
|
### Understanding ToolCallPart and ToolResultPart
|
|
615
618
|
|
|
616
619
|
```typescript
|
|
617
|
-
import * as Prompt from 'effect/
|
|
620
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
618
621
|
|
|
619
622
|
const toolCallPart = Prompt.makePart('tool-call', {
|
|
620
623
|
id: 'call_123',
|
|
@@ -725,8 +728,14 @@ const toolkitLayer = LongRunningToolkit.toLayer({
|
|
|
725
728
|
|
|
726
729
|
**Key Pattern: toolkit.handle**
|
|
727
730
|
|
|
728
|
-
- `toolkit.handle(name, params, toolCallId?)` accepts
|
|
731
|
+
- `toolkit.handle(name, params, toolCallId?, options?)` accepts
|
|
732
|
+
`Tool.ParametersEncoded<T>` and optional schema parse options. Its full shape is
|
|
733
|
+
`Effect<Stream<HandlerResult<T>, Tool.HandlerError<T> | AiError, Tool.HandlerServices<T>>, AiError, Tool.HandlerServices<T>>`.
|
|
729
734
|
- `handle` decodes the encoded input with the tool's parameter schema before invoking the handler; the handler still receives `Tool.Parameters<Tool>`
|
|
735
|
+
- Provide handler services to the outer Effect as well as the Stream. A
|
|
736
|
+
service-dependent parameter decoder runs before the Stream exists; providing
|
|
737
|
+
services only around `Stream.runCollect` is too late. Consuming the Stream
|
|
738
|
+
inside the same provided `Effect.gen` keeps both stages wired.
|
|
730
739
|
- The optional call ID is forwarded to the handler as `context.toolCallId`
|
|
731
740
|
- `preliminary: true`: progress update; do not persist as final history
|
|
732
741
|
- `preliminary: false`: final result to send/persist
|
|
@@ -760,7 +769,7 @@ Passing the decoded `{ times: 3 }` directly to `handle` is now a type error. Use
|
|
|
760
769
|
### Tool Annotations
|
|
761
770
|
|
|
762
771
|
```typescript
|
|
763
|
-
import * as Tool from 'effect/
|
|
772
|
+
import * as Tool from 'effect/ai/Tool';
|
|
764
773
|
import { Schema } from 'effect';
|
|
765
774
|
|
|
766
775
|
const ReadOnlyQuery = Tool.make('query', {
|
|
@@ -786,7 +795,7 @@ MCP emits the first four annotations as tool hints. They are hints, not authoriz
|
|
|
786
795
|
### JSON Schema Generation
|
|
787
796
|
|
|
788
797
|
```typescript
|
|
789
|
-
import * as Tool from 'effect/
|
|
798
|
+
import * as Tool from 'effect/ai/Tool';
|
|
790
799
|
|
|
791
800
|
const tool = Tool.make('example', {
|
|
792
801
|
parameters: Schema.Struct({
|
|
@@ -815,7 +824,7 @@ const jsonSchema = Tool.getJsonSchema(tool);
|
|
|
815
824
|
### Tool Guards
|
|
816
825
|
|
|
817
826
|
```typescript
|
|
818
|
-
import * as Tool from 'effect/
|
|
827
|
+
import * as Tool from 'effect/ai/Tool';
|
|
819
828
|
|
|
820
829
|
const userTool = Tool.make('example');
|
|
821
830
|
const providerTool = Tool.providerDefined({
|
|
@@ -858,8 +867,8 @@ const executeTool = (toolName: string, params: unknown) =>
|
|
|
858
867
|
|
|
859
868
|
<!-- typecheck -->
|
|
860
869
|
```typescript
|
|
861
|
-
import * as Tool from 'effect/
|
|
862
|
-
import * as Toolkit from 'effect/
|
|
870
|
+
import * as Tool from 'effect/ai/Tool';
|
|
871
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
863
872
|
import * as Schema from 'effect/Schema';
|
|
864
873
|
import { Clock, Context, Effect, Layer, Stream } from 'effect';
|
|
865
874
|
|
|
@@ -946,9 +955,9 @@ manual `handle` calls accept their encoded representation.
|
|
|
946
955
|
**CRITICAL**: Always use namespace imports:
|
|
947
956
|
|
|
948
957
|
```typescript
|
|
949
|
-
import * as Tool from 'effect/
|
|
950
|
-
import * as Toolkit from 'effect/
|
|
951
|
-
import * as Prompt from 'effect/
|
|
958
|
+
import * as Tool from 'effect/ai/Tool';
|
|
959
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
960
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
952
961
|
import { Schema, Effect, Data, Context, Layer } from 'effect';
|
|
953
962
|
|
|
954
963
|
const myTool = Tool.make('example');
|
|
@@ -958,8 +967,8 @@ const myToolkit = Toolkit.make(myTool);
|
|
|
958
967
|
**NEVER** do this:
|
|
959
968
|
|
|
960
969
|
```typescript
|
|
961
|
-
import { make } from 'effect/
|
|
962
|
-
import { make as makeToolkit } from 'effect/
|
|
970
|
+
import { make } from 'effect/ai/Tool';
|
|
971
|
+
import { make as makeToolkit } from 'effect/ai/Toolkit';
|
|
963
972
|
```
|
|
964
973
|
|
|
965
974
|
## Quality Checklist
|
|
@@ -3,7 +3,11 @@ name: effect-atom-rpc
|
|
|
3
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
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in `effect/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
|
|
7
|
+
|
|
8
|
+
Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
|
|
9
|
+
Core reactivity APIs are `@stability unstable` and may break in minor releases.
|
|
10
|
+
Use matching Effect-family versions; `@effect/atom-react@4.0.0` requires React 19.
|
|
7
11
|
|
|
8
12
|
## When to use this skill
|
|
9
13
|
|
|
@@ -35,13 +39,13 @@ handle them when consuming the stream. An explicit zero `timeToLive` disables
|
|
|
35
39
|
default idle retention, so unmount/remount may dispose and refetch. Omission uses
|
|
36
40
|
the registry default. HttpApi calls accept per-call `sseOptions`.
|
|
37
41
|
|
|
38
|
-
- `packages/effect/src/
|
|
42
|
+
- `packages/effect/src/reactivity/AtomRpc.ts` — the whole API
|
|
39
43
|
- `packages/effect/test/reactivity/AtomRpc.test.ts` — minimal usage + serialization test
|
|
40
|
-
- `packages/effect/src/
|
|
41
|
-
- `packages/effect/src/
|
|
42
|
-
- `packages/effect/src/
|
|
43
|
-
- `packages/effect/src/
|
|
44
|
-
- `packages/effect/src/
|
|
44
|
+
- `packages/effect/src/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
|
|
45
|
+
- `packages/effect/src/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
|
|
46
|
+
- `packages/effect/src/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
|
|
47
|
+
- `packages/effect/src/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
|
|
48
|
+
- `packages/effect/src/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
|
|
45
49
|
|
|
46
50
|
## Imports
|
|
47
51
|
|
|
@@ -52,9 +56,9 @@ import {
|
|
|
52
56
|
AtomRpc,
|
|
53
57
|
Hydration,
|
|
54
58
|
Reactivity
|
|
55
|
-
} from 'effect/
|
|
56
|
-
import { RpcClient, RpcSerialization } from 'effect/
|
|
57
|
-
import { FetchHttpClient } from 'effect/
|
|
59
|
+
} from 'effect/reactivity';
|
|
60
|
+
import { RpcClient, RpcSerialization } from 'effect/rpc';
|
|
61
|
+
import { FetchHttpClient } from 'effect/http';
|
|
58
62
|
import { useAtomValue, useAtomSet } from '@effect/atom-react';
|
|
59
63
|
```
|
|
60
64
|
|
|
@@ -77,9 +81,9 @@ The `runtime` is what powers `query` and `mutation` — internally it builds a `
|
|
|
77
81
|
|
|
78
82
|
```ts
|
|
79
83
|
import { Layer } from 'effect';
|
|
80
|
-
import { AtomRpc } from 'effect/
|
|
81
|
-
import { FetchHttpClient } from 'effect/
|
|
82
|
-
import { RpcClient, RpcSerialization } from 'effect/
|
|
84
|
+
import { AtomRpc } from 'effect/reactivity';
|
|
85
|
+
import { FetchHttpClient } from 'effect/http';
|
|
86
|
+
import { RpcClient, RpcSerialization } from 'effect/rpc';
|
|
83
87
|
import { UsersGroup } from '../shared/users-rpc.ts';
|
|
84
88
|
|
|
85
89
|
export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
@@ -124,7 +128,7 @@ AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
|
124
128
|
disableTracing: false,
|
|
125
129
|
generateRequestId: customRequestIdFactory, // for deterministic IDs in tests
|
|
126
130
|
makeEffect: undefined, // override the default RpcClient.make
|
|
127
|
-
runtime: Atom.runtime // override
|
|
131
|
+
runtime: Atom.runtime // override with another factory, e.g. Atom.context()
|
|
128
132
|
});
|
|
129
133
|
```
|
|
130
134
|
|
|
@@ -149,6 +153,11 @@ client.query(tag, payload, options?) =>
|
|
|
149
153
|
|
|
150
154
|
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.
|
|
151
155
|
|
|
156
|
+
The key also includes headers, reactivity keys, TTL, and serialization key.
|
|
157
|
+
Use matching options to share a cache entry. Union-of-tag arguments retain the
|
|
158
|
+
selected RPCs' payload/result types; they no longer collapse to `never`. Narrow
|
|
159
|
+
the tag when application logic needs to pair a particular payload with its result.
|
|
160
|
+
|
|
152
161
|
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.
|
|
153
162
|
|
|
154
163
|
```ts
|
|
@@ -184,7 +193,11 @@ client.query(tag, payload, {
|
|
|
184
193
|
```
|
|
185
194
|
|
|
186
195
|
- **`reactivityKeys`** — keys this atom listens to. When a `mutation` (or any `Reactivity.invalidate`) fires with overlapping keys, this atom re-fetches.
|
|
187
|
-
- **`timeToLive`** —
|
|
196
|
+
- **`timeToLive`** — idle retention via `Atom.setIdleTTL`. A finite duration permits
|
|
197
|
+
disposal after inactivity; `Duration.infinity` uses `Atom.keepAlive` until the
|
|
198
|
+
registry is reset/disposed. Omission uses the registry default, while explicit
|
|
199
|
+
zero bypasses default idle retention. Freshness is separate: use `Atom.swr` for
|
|
200
|
+
stale-while-revalidate behavior (see `effect-atom-state`).
|
|
188
201
|
- **`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.
|
|
189
202
|
|
|
190
203
|
### Stream rpcs
|
|
@@ -291,25 +304,27 @@ Hydration uses `Atom.serializable` (which `query` opts into when you pass `seria
|
|
|
291
304
|
|
|
292
305
|
```ts
|
|
293
306
|
// --- server: render then dehydrate ---
|
|
294
|
-
import {
|
|
295
|
-
|
|
296
|
-
const registry = AtomRegistry.make();
|
|
297
|
-
const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
|
|
298
|
-
serializationKey: 'u1'
|
|
299
|
-
});
|
|
307
|
+
import { Effect } from 'effect';
|
|
308
|
+
import { AtomRegistry, Hydration } from 'effect/reactivity';
|
|
300
309
|
|
|
301
|
-
const
|
|
302
|
-
yield* Effect.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
310
|
+
const dehydrated = yield* Effect.scoped(Effect.gen(function* () {
|
|
311
|
+
const registry = yield* Effect.acquireRelease(
|
|
312
|
+
Effect.sync(() => AtomRegistry.make()),
|
|
313
|
+
(registry) => Effect.sync(() => registry.dispose())
|
|
314
|
+
);
|
|
315
|
+
const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
|
|
316
|
+
serializationKey: 'u1'
|
|
317
|
+
});
|
|
318
|
+
yield* AtomRegistry.mount(registry, userAtom);
|
|
319
|
+
yield* AtomRegistry.getResult(registry, userAtom);
|
|
320
|
+
return Hydration.toValues(Hydration.dehydrate(registry));
|
|
321
|
+
}));
|
|
307
322
|
// dehydrated is a JSON-safe array of { key, value } pairs to inline in the HTML
|
|
308
323
|
```
|
|
309
324
|
|
|
310
325
|
```tsx
|
|
311
326
|
// --- client: hydrate before rendering ---
|
|
312
|
-
import { AtomRegistry, Hydration } from 'effect/
|
|
327
|
+
import { AtomRegistry, Hydration } from 'effect/reactivity';
|
|
313
328
|
import { RegistryContext } from '@effect/atom-react';
|
|
314
329
|
|
|
315
330
|
const registry = AtomRegistry.make();
|
|
@@ -327,7 +342,7 @@ Stream rpcs are not serializable — only non-stream `query` atoms with a stable
|
|
|
327
342
|
`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:
|
|
328
343
|
|
|
329
344
|
```ts
|
|
330
|
-
const myRuntime = Atom.
|
|
345
|
+
const myRuntime = Atom.context();
|
|
331
346
|
|
|
332
347
|
export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
333
348
|
group: UsersGroup,
|
|
@@ -336,12 +351,17 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
|
336
351
|
}) {}
|
|
337
352
|
```
|
|
338
353
|
|
|
354
|
+
`withReactivity(keys)` decorates an atom; it is not a runtime factory. Use
|
|
355
|
+
`reactivityKeys` on queries/mutations for invalidation. Runtime factories use
|
|
356
|
+
registry-scoped layer memoization by default; provide the same registry to
|
|
357
|
+
components that should share state.
|
|
358
|
+
|
|
339
359
|
## End-to-end example
|
|
340
360
|
|
|
341
361
|
```ts
|
|
342
362
|
// --- shared/users-rpc.ts ---
|
|
343
363
|
import { Schema } from 'effect';
|
|
344
|
-
import { Rpc, RpcGroup } from 'effect/
|
|
364
|
+
import { Rpc, RpcGroup } from 'effect/rpc';
|
|
345
365
|
|
|
346
366
|
export class User extends Schema.Class<User>('User')({
|
|
347
367
|
id: Schema.String,
|
|
@@ -375,9 +395,9 @@ export const UsersGroup = RpcGroup.make(GetUser, ListUsers, CreateUser);
|
|
|
375
395
|
```ts
|
|
376
396
|
// --- frontend/clients/users-client.ts ---
|
|
377
397
|
import { Layer } from 'effect';
|
|
378
|
-
import { FetchHttpClient } from 'effect/
|
|
379
|
-
import { AtomRpc } from 'effect/
|
|
380
|
-
import { RpcClient, RpcSerialization } from 'effect/
|
|
398
|
+
import { FetchHttpClient } from 'effect/http';
|
|
399
|
+
import { AtomRpc } from 'effect/reactivity';
|
|
400
|
+
import { RpcClient, RpcSerialization } from 'effect/rpc';
|
|
381
401
|
import { UsersGroup } from '../../shared/users-rpc.ts';
|
|
382
402
|
|
|
383
403
|
export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
@@ -393,7 +413,7 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
|
393
413
|
```tsx
|
|
394
414
|
// --- frontend/components/UserList.tsx ---
|
|
395
415
|
import { useAtomValue, useAtomSet } from '@effect/atom-react';
|
|
396
|
-
import { AsyncResult } from 'effect/
|
|
416
|
+
import { AsyncResult } from 'effect/reactivity';
|
|
397
417
|
import { UsersClient } from '../clients/users-client';
|
|
398
418
|
|
|
399
419
|
export function UserList() {
|
|
@@ -455,7 +475,7 @@ export function UserDetail({ id }: { id: string }) {
|
|
|
455
475
|
If your backend exposes an `HttpApi` instead of an `RpcGroup`, `AtomHttpApi.Service<Self>()(id, options)` is the same idea applied to `HttpApiGroup`/`HttpApiEndpoint`:
|
|
456
476
|
|
|
457
477
|
```ts
|
|
458
|
-
import { AtomHttpApi } from 'effect/
|
|
478
|
+
import { AtomHttpApi } from 'effect/reactivity';
|
|
459
479
|
|
|
460
480
|
class ApiClient extends AtomHttpApi.Service<ApiClient>()('ApiClient', {
|
|
461
481
|
api: MyHttpApi,
|
|
@@ -494,7 +514,8 @@ The `query`/`mutation` arguments differ (you pass `groupName, endpointName, requ
|
|
|
494
514
|
- One `AtomRpc.Service` class per logical client; module-singleton, never per-component.
|
|
495
515
|
- Always pass `reactivityKeys` to mutations so dependent queries refresh.
|
|
496
516
|
- Pass `serializationKey` to every query you want SSR-hydratable.
|
|
497
|
-
- Pick `timeToLive` deliberately
|
|
517
|
+
- Pick idle `timeToLive` deliberately; omit for registry defaults, zero to bypass
|
|
518
|
+
default idle retention, or `Duration.infinity` to retain until registry disposal.
|
|
498
519
|
- 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.
|
|
499
520
|
- 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.
|
|
500
521
|
- For typed error recovery, handle declared RPC errors, RPC middleware wire errors, and `RpcClientError`; branch on `_tag` only when the relevant errors are tagged.
|
|
@@ -7,20 +7,20 @@ description: Implement reactive state management with Effect Atom for React appl
|
|
|
7
7
|
|
|
8
8
|
Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.
|
|
9
9
|
|
|
10
|
-
`@effect/atom-react`
|
|
11
|
-
|
|
12
|
-
the Effect release; core atoms still live in `effect/unstable/reactivity`, and
|
|
10
|
+
`@effect/atom-react@4.0.0` requires React `>=19.0.0 <20.0.0`.
|
|
11
|
+
Keep the adapter at the same version as `effect`; core atoms live in `effect/reactivity`, and
|
|
13
12
|
React bindings live in `@effect/atom-react` (`packages/atom/react` upstream).
|
|
13
|
+
Core reactivity APIs carry `@stability unstable` and may break in minor releases.
|
|
14
14
|
|
|
15
15
|
## Effect Source Reference
|
|
16
16
|
|
|
17
17
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
18
|
-
|
|
18
|
+
Inspect `git show effect@4.0.0:<path>` there for this baseline; main may be ahead.
|
|
19
19
|
|
|
20
20
|
Reference this for:
|
|
21
21
|
|
|
22
|
-
- Atom reactivity: `packages/effect/src/
|
|
23
|
-
- AsyncResult source: `packages/effect/src/
|
|
22
|
+
- Atom reactivity: `packages/effect/src/reactivity/`
|
|
23
|
+
- AsyncResult source: `packages/effect/src/reactivity/AsyncResult.ts`
|
|
24
24
|
- Effect source: `packages/effect/src/`
|
|
25
25
|
|
|
26
26
|
## Core Concepts
|
|
@@ -36,7 +36,7 @@ explicit for state that must survive SSR dehydration.
|
|
|
36
36
|
Atoms work **by reference** - they are stable containers for reactive state:
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import * as Atom from 'effect/
|
|
39
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
40
40
|
|
|
41
41
|
// Atoms are created once and referenced throughout the app
|
|
42
42
|
export const counterAtom = Atom.make(0);
|
|
@@ -47,10 +47,13 @@ export const counterAtom = Atom.make(0);
|
|
|
47
47
|
|
|
48
48
|
### Automatic Cleanup
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Registry nodes become eligible for disposal when unused by subscribers and
|
|
51
|
+
dependent atoms. Disposal follows the atom's idle TTL or registry default; it is
|
|
52
|
+
not necessarily synchronous with the last React unmount. `keepAlive` retains the
|
|
53
|
+
node until its registry is reset/disposed:
|
|
51
54
|
|
|
52
55
|
```typescript
|
|
53
|
-
//
|
|
56
|
+
// Disposed after becoming unused, according to the registry's idle policy
|
|
54
57
|
export const temporaryState = Atom.make(initialValue);
|
|
55
58
|
|
|
56
59
|
// Persists across component lifecycles
|
|
@@ -61,10 +64,15 @@ export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive);
|
|
|
61
64
|
|
|
62
65
|
Atom values are computed on-demand when subscribers access them.
|
|
63
66
|
|
|
67
|
+
Tracked dependencies remain retained while a node is stale and are reconciled
|
|
68
|
+
on its next build. Register cleanup with `get.addFinalizer` or scoped effects so
|
|
69
|
+
superseded builds release resources. Observed failed builds can recover when a
|
|
70
|
+
dependency changes; avoid manual reset workarounds for stale-node propagation.
|
|
71
|
+
|
|
64
72
|
## Pattern: Basic Atoms
|
|
65
73
|
|
|
66
74
|
```typescript
|
|
67
|
-
import * as Atom from 'effect/
|
|
75
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
68
76
|
|
|
69
77
|
// Simple atom
|
|
70
78
|
export const count = Atom.make(0);
|
|
@@ -132,19 +140,29 @@ export const userAtoms = Atom.family((userId: string) =>
|
|
|
132
140
|
Atom.make<User | null>(null).pipe(Atom.keepAlive)
|
|
133
141
|
);
|
|
134
142
|
|
|
135
|
-
//
|
|
143
|
+
// Reuses the cached live atom for a given ID
|
|
136
144
|
const userAtom = userAtoms(userId);
|
|
137
145
|
```
|
|
138
146
|
|
|
147
|
+
Family caches use weak references where supported. Identity is reused while the
|
|
148
|
+
cached atom is live; it is not a permanent store of every key ever requested.
|
|
149
|
+
An old atom's finalizer does not evict a newer cached atom for the same key.
|
|
150
|
+
|
|
139
151
|
## Pattern: Atom.fn for Async Actions
|
|
140
152
|
|
|
141
153
|
Use `Atom.fn` with `Effect.fnUntraced` for async operations:
|
|
142
154
|
|
|
143
155
|
- Reading gives `AsyncResult<Success, Error>` with automatic `.waiting` flag
|
|
144
156
|
- Triggering via `useAtomSet` runs the effect
|
|
157
|
+
- The callback takes one application argument plus an `Atom.FnContext`; bundle
|
|
158
|
+
multiple inputs into an object. The second argument is not another payload.
|
|
159
|
+
- By default a new write replaces the current run. Use `{ concurrent: true }`
|
|
160
|
+
deliberately for overlapping Effect executions. Synchronous successes and
|
|
161
|
+
failures are retained in concurrent mode; it still exposes one `AsyncResult`,
|
|
162
|
+
not a per-invocation result log.
|
|
145
163
|
|
|
146
164
|
```typescript
|
|
147
|
-
import * as Atom from "effect/
|
|
165
|
+
import * as Atom from "effect/reactivity/Atom"
|
|
148
166
|
import { useAtomValue, useAtomSet } from "@effect/atom-react"
|
|
149
167
|
import { Effect, Exit } from "effect"
|
|
150
168
|
|
|
@@ -278,7 +296,7 @@ Atom.runtime.addGlobalLayer(
|
|
|
278
296
|
Atoms can return `AsyncResult` types for explicit error handling:
|
|
279
297
|
|
|
280
298
|
```tsx
|
|
281
|
-
import * as AsyncResult from 'effect/
|
|
299
|
+
import * as AsyncResult from 'effect/reactivity/AsyncResult';
|
|
282
300
|
|
|
283
301
|
export const userData = Atom.make<AsyncResult.AsyncResult<User, Error>>(
|
|
284
302
|
AsyncResult.initial()
|
|
@@ -354,7 +372,7 @@ Use `Atom.kvs` for persisted state:
|
|
|
354
372
|
|
|
355
373
|
```typescript
|
|
356
374
|
import { BrowserKeyValueStore as BrowserKvs } from '@effect/platform-browser';
|
|
357
|
-
import * as Atom from 'effect/
|
|
375
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
358
376
|
import * as Schema from 'effect/Schema';
|
|
359
377
|
|
|
360
378
|
export const userSettings = Atom.kvs({
|
|
@@ -452,7 +470,7 @@ export const wsConnection = Atom.make(
|
|
|
452
470
|
2. **Never Manual Void Wrappers**: Don't wrap Effects in void functions—you lose `waiting` control
|
|
453
471
|
3. **Reference Stability**: Use `Atom.family` for dynamically generated atom sets
|
|
454
472
|
4. **Lazy Evaluation**: Values computed on-demand when accessed
|
|
455
|
-
5. **Automatic Cleanup**:
|
|
473
|
+
5. **Automatic Cleanup**: Unused atoms follow idle retention; `keepAlive` lasts until registry reset/disposal
|
|
456
474
|
6. **Derive, Don't Coordinate**: Use computed atoms to derive state
|
|
457
475
|
7. **Result Types**: Handle errors explicitly with AsyncResult.match
|
|
458
476
|
8. **Services in Runtime**: Wrap layers once, use in multiple atoms
|
|
@@ -467,7 +485,7 @@ export const wsConnection = Atom.make(
|
|
|
467
485
|
Use `Atom.fn` with `Effect.fnUntraced` which automatically provides `AsyncResult` with `.waiting` flag:
|
|
468
486
|
|
|
469
487
|
```typescript
|
|
470
|
-
import * as Atom from "effect/
|
|
488
|
+
import * as Atom from "effect/reactivity/Atom"
|
|
471
489
|
import { useAtomValue, useAtomSet } from "@effect/atom-react"
|
|
472
490
|
import { Effect } from "effect"
|
|
473
491
|
|
|
@@ -496,7 +514,7 @@ function UserProfile() {
|
|
|
496
514
|
|
|
497
515
|
```typescript
|
|
498
516
|
export const updateItem = runtime.fn(
|
|
499
|
-
Effect.fnUntraced(function* (id: string
|
|
517
|
+
Effect.fnUntraced(function* ({ id, updates }: { id: string; updates: Partial<Item> }) {
|
|
500
518
|
const current = yield* Atom.get(itemsAtom);
|
|
501
519
|
|
|
502
520
|
// Optimistic update
|
|
@@ -569,12 +587,32 @@ Atom.batch(() => {
|
|
|
569
587
|
registry.set(ageAtom, 30);
|
|
570
588
|
registry.set(statusAtom, 'active');
|
|
571
589
|
});
|
|
572
|
-
//
|
|
590
|
+
// Dependents observe the final batched state
|
|
573
591
|
```
|
|
574
592
|
|
|
575
593
|
Outside Effect/Atom contexts, use an `AtomRegistry` (`registry.set(...)`) inside the batch. Inside an atom or write context, use that context (`ctx.set(...)`) in the same pattern. `Atom.batch` only batches notifications; it does not introduce a free `set` function.
|
|
576
594
|
|
|
577
|
-
|
|
595
|
+
Batch synchronous writes to avoid intermediate notifications. Writes made by
|
|
596
|
+
commit listeners are processed in subsequent commit work rather than dropped,
|
|
597
|
+
so do not promise exactly one callback when listeners themselves write.
|
|
598
|
+
Batching is not rollback: writes made before a thrown exception are still
|
|
599
|
+
committed and notified, then the first failure is rethrown. Async work after an
|
|
600
|
+
`await` is outside the batch.
|
|
601
|
+
|
|
602
|
+
## Stale-while-revalidate reads
|
|
603
|
+
|
|
604
|
+
Wrap an `AsyncResult` query atom with `Atom.swr({ staleTime: '30 seconds' })`.
|
|
605
|
+
Reads return the current result and defer stale-source refresh until after the
|
|
606
|
+
read. The scheduled refresh is skipped if the source becomes fresh or the
|
|
607
|
+
wrapper is disposed; a one-shot unmounted read does not keep background work
|
|
608
|
+
alive. Mount/subscribe for a continuing query lifetime.
|
|
609
|
+
|
|
610
|
+
`revalidateOnMount` controls initial stale refreshes. `revalidateOnFocus: true`
|
|
611
|
+
respects `staleTime`, while `'always'` forces a refresh. Manual refresh always
|
|
612
|
+
forwards to the source. `staleTime` is a freshness window, distinct from idle TTL.
|
|
613
|
+
Create the wrapper once (module scope, `Atom.family`, or `useMemo`) rather than
|
|
614
|
+
on every render. `swr` returns `WithoutSerializable<R>`; retain the serializable
|
|
615
|
+
source atom for hydration or explicitly serialize the wrapper under its own key.
|
|
578
616
|
|
|
579
617
|
## AsyncResult.builder
|
|
580
618
|
|
|
@@ -14,7 +14,7 @@ Reference this for:
|
|
|
14
14
|
|
|
15
15
|
- `Request` and `Request.Class` definitions (`packages/effect/src/Request.ts`)
|
|
16
16
|
- `RequestResolver` constructors and combinators (`packages/effect/src/RequestResolver.ts`)
|
|
17
|
-
- `SqlResolver` for SQL-specific batching (`packages/effect/src/
|
|
17
|
+
- `SqlResolver` for SQL-specific batching (`packages/effect/src/sql/SqlResolver.ts`)
|
|
18
18
|
- Batching tutorial (`ai-docs/src/05_batching/10_request-resolver.ts`)
|
|
19
19
|
|
|
20
20
|
## The N+1 Problem
|
|
@@ -440,10 +440,12 @@ class Users extends Context.Service<
|
|
|
440
440
|
|
|
441
441
|
## SQL Integration with SqlResolver
|
|
442
442
|
|
|
443
|
-
`SqlResolver` (from `effect/
|
|
443
|
+
`SqlResolver` (from `effect/sql`) provides schema-validated, batched SQL resolvers.
|
|
444
|
+
It remains marked `@stability unstable` even though its import path has no
|
|
445
|
+
`unstable` segment; check its release-specific contract when upgrading. Import:
|
|
444
446
|
|
|
445
447
|
```typescript
|
|
446
|
-
import { SqlResolver } from 'effect/
|
|
448
|
+
import { SqlResolver } from 'effect/sql';
|
|
447
449
|
```
|
|
448
450
|
|
|
449
451
|
### SqlResolver.ordered
|