opencode-effect-enforcer 0.2.8 → 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 +8 -6
- package/guidance/progressive-disclosure-guidance.md +15 -7
- 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 +1 -1
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +3 -3
- 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
|
@@ -5,6 +5,10 @@ description: Master Effect AI streaming response patterns including start/delta/
|
|
|
5
5
|
|
|
6
6
|
# Effect AI Streaming
|
|
7
7
|
|
|
8
|
+
Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
|
|
9
|
+
The `effect/ai` APIs remain `@stability unstable` despite their shorter import
|
|
10
|
+
paths and may break in minor releases. Align provider package versions with `effect`.
|
|
11
|
+
|
|
8
12
|
## When to Use This Skill
|
|
9
13
|
|
|
10
14
|
- Real-time streaming responses from language models
|
|
@@ -24,7 +28,7 @@ import * as Effect from 'effect/Effect';
|
|
|
24
28
|
import * as Channel from 'effect/Channel';
|
|
25
29
|
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
26
30
|
import * as Match from 'effect/Match';
|
|
27
|
-
import * as Response from 'effect/
|
|
31
|
+
import * as Response from 'effect/ai/Response';
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
## StreamPart Protocol
|
|
@@ -80,8 +84,8 @@ Accumulate stream parts incrementally using mutable state for efficiency:
|
|
|
80
84
|
```typescript
|
|
81
85
|
import * as Stream from 'effect/Stream';
|
|
82
86
|
import * as Effect from 'effect/Effect';
|
|
83
|
-
import * as Prompt from 'effect/
|
|
84
|
-
import * as Response from 'effect/
|
|
87
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
88
|
+
import * as Response from 'effect/ai/Response';
|
|
85
89
|
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
86
90
|
|
|
87
91
|
const streamWithHistory = Stream.suspend(() => {
|
|
@@ -214,14 +218,20 @@ Why checkpoint-based merging:
|
|
|
214
218
|
- `Prompt.fromResponseParts` routes framework-executed final results into a tool message, but keeps provider-executed final results in the assistant message. It preserves `providerExecuted` and uses `encodedResult` in both cases.
|
|
215
219
|
- Tools requiring approval emit `tool-approval-request`. Append a matching `Prompt.toolApprovalResponsePart` in a tool message and call the model again; approved/denied responses are pre-resolved into final tool results before the next provider call.
|
|
216
220
|
- In OpenAI-specific SSE code, unknown future events decode through `OpenAiSchema.ResponseStreamEvent` and are ignored by `OpenAiLanguageModel`; malformed known events still fail decoding.
|
|
221
|
+
- OpenAI-compatible Chat Completions providers can send null delta fields. The
|
|
222
|
+
adapter preserves text and tool arguments from the remaining fields; custom
|
|
223
|
+
adapters should skip null fragments rather than discard the whole chunk.
|
|
224
|
+
- Manual `Toolkit.handle` execution requires handler services on both the outer
|
|
225
|
+
Effect and its returned Stream. Even return-mode streams can fail with
|
|
226
|
+
`AiError` during result encoding; keep that error channel when composing streams.
|
|
217
227
|
|
|
218
228
|
## Complete Example
|
|
219
229
|
|
|
220
230
|
<!-- typecheck -->
|
|
221
231
|
```typescript
|
|
222
|
-
import * as Prompt from 'effect/
|
|
223
|
-
import * as Response from 'effect/
|
|
224
|
-
import * as LanguageModel from 'effect/
|
|
232
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
233
|
+
import * as Response from 'effect/ai/Response';
|
|
234
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
225
235
|
import * as Stream from 'effect/Stream';
|
|
226
236
|
import * as Channel from 'effect/Channel';
|
|
227
237
|
import * as Effect from 'effect/Effect';
|
|
@@ -327,16 +337,21 @@ Stream.mapArrayEffect(Effect.fnUntraced(function* (chunk) {
|
|
|
327
337
|
### Source Parts
|
|
328
338
|
|
|
329
339
|
```typescript
|
|
330
|
-
{ type: "
|
|
331
|
-
{ type: "
|
|
340
|
+
{ type: "source", sourceType: "document", id: string, mediaType: string, title: string }
|
|
341
|
+
{ type: "source", sourceType: "url", id: string, url: URL, title: string }
|
|
332
342
|
```
|
|
333
343
|
|
|
344
|
+
The encoded URL source uses a string URL.
|
|
345
|
+
|
|
334
346
|
### Metadata Parts
|
|
335
347
|
|
|
336
348
|
```typescript
|
|
337
|
-
{ type: "response-metadata", id
|
|
349
|
+
{ type: "response-metadata", id?: string, modelId?: string, timestamp?: DateTime.Utc }
|
|
338
350
|
```
|
|
339
351
|
|
|
352
|
+
The encoded provider form uses an ISO string for `timestamp`; the decoded part
|
|
353
|
+
uses `DateTime.Utc`.
|
|
354
|
+
|
|
340
355
|
### Error Parts
|
|
341
356
|
|
|
342
357
|
```typescript
|
|
@@ -383,9 +398,9 @@ StreamPart types:
|
|
|
383
398
|
|
|
384
399
|
Key modules:
|
|
385
400
|
|
|
386
|
-
- `effect/
|
|
387
|
-
- `effect/
|
|
388
|
-
- `effect/Stream` - Stream combinators (`
|
|
401
|
+
- `effect/ai/Response` - Response part schemas and constructors
|
|
402
|
+
- `effect/ai/Prompt` - Prompt construction and merging
|
|
403
|
+
- `effect/Stream` - Stream combinators (`mapArrayEffect`, `runForEach`, `runDrain`)
|
|
389
404
|
- `effect/Channel` - Low-level resource management (`acquireUseRelease`)
|
|
390
405
|
- `effect/SubscriptionRef` - Reactive shared state
|
|
391
406
|
- `effect/Match` - Pattern matching (use `Match.when({ type: ... })` for stream parts)
|
|
@@ -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.
|