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
|
@@ -3,25 +3,25 @@ name: effect-rpc-server
|
|
|
3
3
|
description: Serve RpcGroup contracts with Effect's RpcServer — handler layers (RpcGroup.toLayer/toLayerHandler), protocol layers (HTTP, WebSocket, TCP, stdio, worker), RpcSerialization, server middleware, streaming results, interruption and shutdown semantics, RpcTest. Use when implementing the server side of an Effect RPC API, mounting RPC on an HttpRouter or existing HTTP app, choosing a wire format, implementing RpcMiddleware, handling client aborts, or testing RPC handlers.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in serving RPC groups with `RpcServer` from `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in serving RPC groups with `RpcServer` from `effect/rpc`.
|
|
7
7
|
|
|
8
|
-
Everything ships from the `effect` package under `effect/
|
|
8
|
+
Everything ships from the `effect` package under `effect/rpc` — there is no `@effect/rpc` package in v4. This skill covers the **server side**: implementing handlers, transports, serialization, middleware, and lifecycle. For defining `Rpc`/`RpcGroup` contracts see the `effect-rpc-api` skill; for building clients see the `effect-rpc-client` skill; for cluster entities see the `effect-rpc-cluster` skill; for `HttpRouter`/`HttpServer` fundamentals see the `effect-http-server` skill.
|
|
9
9
|
|
|
10
10
|
## Effect Source Reference
|
|
11
11
|
|
|
12
|
-
The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read
|
|
12
|
+
The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read the `effect@4.0.0` tag for this skill; main may be newer. These APIs remain `@stability unstable` and may break in minor releases. Keep Effect-family packages on the same release.
|
|
13
13
|
|
|
14
14
|
Key files:
|
|
15
15
|
|
|
16
|
-
- `packages/effect/src/
|
|
17
|
-
- `packages/effect/src/
|
|
18
|
-
- `packages/effect/src/
|
|
19
|
-
- `packages/effect/src/
|
|
20
|
-
- `packages/effect/src/
|
|
21
|
-
- `packages/effect/src/
|
|
22
|
-
- `packages/effect/src/
|
|
23
|
-
- `packages/effect/src/
|
|
24
|
-
- `packages/effect/src/
|
|
16
|
+
- `packages/effect/src/rpc/RpcServer.ts` — `make`, `makeNoSerialization`, `layer`, `layerHttp`, every `layerProtocol*` / `makeProtocol*`, `toHttpEffect*`, the `Protocol` service
|
|
17
|
+
- `packages/effect/src/rpc/RpcGroup.ts` — `toLayer`, `toLayerHandler`, `toHandlers`, `accessHandler`, `of`, handler type derivation
|
|
18
|
+
- `packages/effect/src/rpc/Rpc.ts` — `ServerClient`, `Handler`, `ToHandlerFn`, `ResultFrom`, `fork`, `uninterruptible`, `ServicesServer`
|
|
19
|
+
- `packages/effect/src/rpc/RpcMiddleware.ts` — `Service` constructor, server middleware function shape, `layerClient`
|
|
20
|
+
- `packages/effect/src/rpc/RpcSerialization.ts` — JSON, NDJSON, JSON-RPC, and SchemaBinary parsers and layers
|
|
21
|
+
- `packages/effect/src/rpc/RpcMessage.ts` — the wire vocabulary (`Request`, `Ack`, `Interrupt`, `Eof`, `Chunk`, `Exit`, `Defect`, `ClientEnd`)
|
|
22
|
+
- `packages/effect/src/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
|
|
23
|
+
- `packages/effect/src/rpc/RpcTest.ts` — in-process test client
|
|
24
|
+
- `packages/effect/src/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
|
|
25
25
|
- `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — the best end-to-end reference for real wiring across http/ws/tcp transports and every serialization
|
|
26
26
|
- `packages/platform/browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
|
|
27
27
|
|
|
@@ -60,7 +60,7 @@ Imports used throughout:
|
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
62
|
import { Cause, Context, Deferred, Effect, Layer, Queue, Schema, Stream } from 'effect';
|
|
63
|
-
import { Headers, HttpRouter } from 'effect/
|
|
63
|
+
import { Headers, HttpRouter } from 'effect/http';
|
|
64
64
|
import {
|
|
65
65
|
Rpc,
|
|
66
66
|
RpcGroup,
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
RpcServer,
|
|
72
72
|
RpcTest,
|
|
73
73
|
RpcWorker
|
|
74
|
-
} from 'effect/
|
|
74
|
+
} from 'effect/rpc';
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Running example group (definition details belong to the `effect-rpc-api` skill):
|
|
@@ -255,6 +255,14 @@ const ServerLayer = RpcServer.layerHttp({
|
|
|
255
255
|
|
|
256
256
|
The protocol layers register routes on `HttpRouter`; `HttpRouter.serve` provides the router and turns it into an HTTP app:
|
|
257
257
|
|
|
258
|
+
Keep the protocol/server composition inside the app passed to `serve` (or
|
|
259
|
+
`toWebHandler`/`toHttpEffect`). `serve` and `toHttpEffect` build a fresh router in a
|
|
260
|
+
forked layer memo map; `toWebHandler` builds separately by default but uses any
|
|
261
|
+
explicit `memoMap` as supplied. Do not pre-provide `HttpRouter.layer` to the RPC route layer;
|
|
262
|
+
that registers on a router the entrypoint does not serve. If sibling servers need
|
|
263
|
+
the same stateful handler dependencies, provide those services outside their
|
|
264
|
+
entrypoints; layers first built inside an app are private to that app.
|
|
265
|
+
|
|
258
266
|
```ts
|
|
259
267
|
import { createServer } from 'node:http';
|
|
260
268
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
@@ -268,6 +276,27 @@ NodeRuntime.runMain(Layer.launch(Main));
|
|
|
268
276
|
|
|
269
277
|
See the `effect-http-server` skill for `HttpRouter.serve` options (middleware, `disableLogger`, `disableListenLog`) and adding sibling routes.
|
|
270
278
|
|
|
279
|
+
The same route composition can be built without any platform dependency:
|
|
280
|
+
|
|
281
|
+
<!-- typecheck -->
|
|
282
|
+
```ts
|
|
283
|
+
import { Effect, Layer } from 'effect';
|
|
284
|
+
import * as Schema from 'effect/Schema';
|
|
285
|
+
import { HttpRouter } from 'effect/http';
|
|
286
|
+
import { Rpc, RpcGroup, RpcSerialization, RpcServer } from 'effect/rpc';
|
|
287
|
+
|
|
288
|
+
const Pings = RpcGroup.make(Rpc.make('Ping', { success: Schema.String }));
|
|
289
|
+
const Handlers = Pings.toLayer({ Ping: () => Effect.succeed('pong') });
|
|
290
|
+
const Routes = RpcServer.layerHttp({
|
|
291
|
+
group: Pings,
|
|
292
|
+
path: '/rpc',
|
|
293
|
+
protocol: 'http'
|
|
294
|
+
}).pipe(Layer.provide([Handlers, RpcSerialization.layerNdjson]));
|
|
295
|
+
|
|
296
|
+
// Keep this construction scope alive while using the returned HTTP effect.
|
|
297
|
+
const makeHttpEffect = HttpRouter.toHttpEffect(Routes);
|
|
298
|
+
```
|
|
299
|
+
|
|
271
300
|
---
|
|
272
301
|
|
|
273
302
|
## 3. Protocol Layers — Picking a Transport
|
|
@@ -386,6 +415,12 @@ Rules, verified against the protocol implementations and the e2e matrix:
|
|
|
386
415
|
|
|
387
416
|
Client and server must use the **same** serialization.
|
|
388
417
|
|
|
418
|
+
NDJSON skips malformed lines so later frames still decode; JSON-RPC skips
|
|
419
|
+
non-object messages and checks notification method types before interpreting
|
|
420
|
+
internal messages. Continue validating procedure payloads through the contract.
|
|
421
|
+
Custom encoded-interrupt consumers must allow `fiberId: null` as well as
|
|
422
|
+
`undefined`, matching JSON-encoded `RpcMessage.ExitEncoded` values.
|
|
423
|
+
|
|
389
424
|
---
|
|
390
425
|
|
|
391
426
|
## 5. Embedding in an Existing HTTP App
|
|
@@ -512,7 +547,7 @@ StreamUsers: Effect.fnUntraced(function* (payload) {
|
|
|
512
547
|
Semantics, verified against `RpcServer.makeNoSerialization`:
|
|
513
548
|
|
|
514
549
|
- The server batches available values into `Chunk` messages (`Stream.runForEachArray` / `Queue.takeAll`), so one message can carry many elements.
|
|
515
|
-
- **
|
|
550
|
+
- **RPC acknowledgment backpressure** exists on ack-supporting transports: after writing a chunk the server waits for the client's `Ack` before pulling more. Framed HTTP has no RPC acks but still backpressures producers through its bounded response queue (`streamBufferSize`, default 16).
|
|
516
551
|
- **Ending**: a `Stream` ending ends the response; for the queue form call `Queue.end(queue)` — `Cause.Done` is the end-of-stream signal, and `Queue.end` only typechecks when the queue's error channel includes it, so create the queue as `Queue.bounded<User, Cause.Done>(16)`. The client then sees the stream complete with a final void `Exit`.
|
|
517
552
|
- **Failures**: failing the stream with the rpc's declared error fails the client's stream with that typed error.
|
|
518
553
|
- The client consumes the result as a `Stream` by default or a `Queue.Dequeue` with `{ asQueue: true }` — see the `effect-rpc-client` skill.
|
|
@@ -545,6 +580,7 @@ Never: () =>
|
|
|
545
580
|
|
|
546
581
|
- **HTTP**: if the request's scope closes before responses finish (client went away), the protocol sends an `Interrupt` for every request id from that HTTP call.
|
|
547
582
|
- **Sockets/WebSockets**: a closed connection is pushed to the protocol's `disconnects` queue; the server interrupts all of that client's in-flight fibers.
|
|
583
|
+
- **HTTP WebSocket upgrades**: Node/Bun scope cleanup sends `1000` on success, `1001` for interruption-only exits, or `1011` on failure, preserving an explicit close code already sent. The HTTP request scope retains the handler failure through error-response handling and observation middleware.
|
|
548
584
|
- An `Interrupt` for an unknown/finished request id is answered with `Exit.interrupt()` — interruption is idempotent.
|
|
549
585
|
|
|
550
586
|
### `Rpc.uninterruptible`
|
|
@@ -572,7 +608,7 @@ Worker entrypoint (browser shown; for Node use `NodeWorkerRunner.layer` from `@e
|
|
|
572
608
|
// worker.ts
|
|
573
609
|
import { BrowserWorkerRunner } from '@effect/platform-browser';
|
|
574
610
|
import { Effect, Layer } from 'effect';
|
|
575
|
-
import { RpcServer } from 'effect/
|
|
611
|
+
import { RpcServer } from 'effect/rpc';
|
|
576
612
|
|
|
577
613
|
const MainLive = RpcServer.layer(UserRpcs).pipe(
|
|
578
614
|
Layer.provide(UsersLive),
|
|
@@ -609,7 +645,7 @@ Wires `RpcServer.makeNoSerialization` to a no-serialization client. Requests, st
|
|
|
609
645
|
|
|
610
646
|
```ts
|
|
611
647
|
import { assert, it } from '@effect/vitest';
|
|
612
|
-
import { RpcClient } from 'effect/
|
|
648
|
+
import { RpcClient } from 'effect/rpc';
|
|
613
649
|
|
|
614
650
|
class UsersClient extends Context.Service<
|
|
615
651
|
UsersClient,
|
|
@@ -684,8 +720,8 @@ The decoded core with no transport at all: you push `FromClient<Rpcs>` messages
|
|
|
684
720
|
import { createServer } from 'node:http';
|
|
685
721
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
686
722
|
import { Effect, Layer } from 'effect';
|
|
687
|
-
import { HttpRouter } from 'effect/
|
|
688
|
-
import { RpcSerialization, RpcServer } from 'effect/
|
|
723
|
+
import { HttpRouter } from 'effect/http';
|
|
724
|
+
import { RpcSerialization, RpcServer } from 'effect/rpc';
|
|
689
725
|
import { SecureRpcs } from '../domain/rpc.ts';
|
|
690
726
|
|
|
691
727
|
const UsersLive = SecureRpcs.toLayer(
|
|
@@ -773,7 +809,7 @@ const ServerLayer = RpcServer.layer(StoreRpcs, { concurrency: 1 }).pipe(
|
|
|
773
809
|
|
|
774
810
|
## Common Mistakes
|
|
775
811
|
|
|
776
|
-
1. **Importing from `@effect/rpc`.** v3 habit; the package does not exist in v4. Everything is `effect/
|
|
812
|
+
1. **Importing from `@effect/rpc`.** v3 habit; the package does not exist in v4. Everything is `effect/rpc` (and platform layers come from `@effect/platform-node` / `-bun` / `-browser`).
|
|
777
813
|
2. **Forgetting `protocol: 'http'` on `layerHttp`.** The default is `'websocket'` — your `POST /rpc` curl returns 404 and only a `GET` upgrade route exists. Also note `layerHttp` takes `{ group, path, ... }` as one options bag, while `layer(group, options)` takes the group positionally.
|
|
778
814
|
3. **Providing handlers/middleware but no `Protocol` or `RpcSerialization`.** `RpcServer.layer` requires all of: handler layer(s), middleware implementation layers, a `layerProtocol*`, and (for non-worker protocols) a `RpcSerialization.layer*`. Missing ones surface as unresolved layer requirements.
|
|
779
815
|
4. **`layerJson` on a raw TCP socket server.** Sockets need `layerNdjson`, `layerNdJsonRpc()`, or `layerSchemaBinary()` for framing. WebSocket supplies its own framing and supports `layerJson`.
|
|
@@ -7,7 +7,11 @@ You are an Effect TypeScript expert specializing in `Schedule`, retry, repeat, p
|
|
|
7
7
|
|
|
8
8
|
## Source Of Truth
|
|
9
9
|
|
|
10
|
-
Verify APIs against
|
|
10
|
+
Verify APIs against `packages/effect/src/Schedule.ts` and `Effect.ts` at the
|
|
11
|
+
installed release tag in `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
12
|
+
The `effect@4.0.0` tag has no `cookbooks/schedule.md`; its runnable schedule guide
|
|
13
|
+
is `ai-docs/src/06_schedule/10_schedules.ts`. In Effect v4, `Schedule.concat` is
|
|
14
|
+
current, `Schedule.tapInput` is absent, and `Schedule.tap` receives full metadata.
|
|
11
15
|
|
|
12
16
|
## Semantics
|
|
13
17
|
|
|
@@ -22,12 +26,14 @@ incorrect delay, retryAfter, and resetAfter values.
|
|
|
22
26
|
- `Effect.repeat` reruns successes. A typed failure stops repetition unless the pass handles it first.
|
|
23
27
|
- The source effect runs once before the schedule is stepped.
|
|
24
28
|
- `Schedule.recurs(3)` permits three recurrences after the initial evaluation: at most four evaluations total.
|
|
29
|
+
- `Schedule.once` permits one immediate recurrence and outputs `void` on both recurrence and completion. `Effect.repeat(task, Schedule.once)` evaluates `task` twice, not once.
|
|
25
30
|
- Schedules can require services and fail; schedule errors join the resulting effect's error channel.
|
|
26
31
|
- Retry only the narrowest idempotent operation. Never retry non-idempotent writes unless an idempotency key, transaction, or equivalent guarantee makes replay safe.
|
|
27
32
|
|
|
28
33
|
## Policy Chooser
|
|
29
34
|
|
|
30
35
|
- Counter only: `Schedule.recurs(n)`.
|
|
36
|
+
- One immediate recurrence, no counter output: `Schedule.once` (a value, not a function).
|
|
31
37
|
- Delay after each completed run: `Schedule.spaced(duration)`.
|
|
32
38
|
- Cadence aligned to time boundaries: `Schedule.fixed(interval)`; slow work may make the next run immediate, and missed ticks are not replayed.
|
|
33
39
|
- Backoff: `Schedule.exponential(base)` or `Schedule.fibonacci(base)`.
|
|
@@ -75,6 +81,28 @@ const numericDates = mixed.pipe(
|
|
|
75
81
|
|
|
76
82
|
## Polling And Item Failure Policy
|
|
77
83
|
|
|
84
|
+
### Bounded repetition and refinement
|
|
85
|
+
|
|
86
|
+
With predicate-only `Effect.repeat`, an `until` refinement narrows the final
|
|
87
|
+
result to the matched type; a `while` refinement excludes the continuing type.
|
|
88
|
+
Adding `times` or `schedule` preserves the **full source result type** because
|
|
89
|
+
the bound can stop repetition before the predicate does. This also applies when
|
|
90
|
+
`times` is optional in the options type. Check the returned value before using
|
|
91
|
+
fields that only exist on the desired terminal variant.
|
|
92
|
+
|
|
93
|
+
<!-- typecheck -->
|
|
94
|
+
```ts
|
|
95
|
+
import { Effect, Schedule } from 'effect';
|
|
96
|
+
import * as Option from 'effect/Option';
|
|
97
|
+
|
|
98
|
+
declare const poll: Effect.Effect<Option.Option<string>>;
|
|
99
|
+
|
|
100
|
+
const bounded: Effect.Effect<Option.Option<string>> = poll.pipe(
|
|
101
|
+
Effect.repeat({ until: Option.isSome, times: 3 })
|
|
102
|
+
);
|
|
103
|
+
const twice = Effect.repeat(Effect.succeed('tick'), Schedule.once);
|
|
104
|
+
```
|
|
105
|
+
|
|
78
106
|
Use `Effect.repeat(pass, Schedule.spaced(...))` for a worker that emits no meaningful values. Use `Stream.fromEffectSchedule` when each result is part of a stream pipeline.
|
|
79
107
|
|
|
80
108
|
Decide failures at the correct granularity:
|
|
@@ -164,13 +164,13 @@ import { Schema } from 'effect';
|
|
|
164
164
|
Schema.String.check(Schema.isMaxLength(5));
|
|
165
165
|
Schema.String.check(Schema.isMinLength(5));
|
|
166
166
|
Schema.String.check(Schema.isNonEmpty()); // non-empty string
|
|
167
|
-
Schema.String.check(Schema.
|
|
167
|
+
Schema.String.check(Schema.isBetweenLength(2, 4));
|
|
168
168
|
|
|
169
169
|
// Pattern matching
|
|
170
170
|
Schema.String.check(Schema.isPattern(/^[a-z]+$/));
|
|
171
|
-
Schema.String.check(Schema.
|
|
172
|
-
Schema.String.check(Schema.
|
|
173
|
-
Schema.String.check(Schema.
|
|
171
|
+
Schema.String.check(Schema.isStartingWith('prefix'));
|
|
172
|
+
Schema.String.check(Schema.isEndingWith('suffix'));
|
|
173
|
+
Schema.String.check(Schema.isIncluding('substring'));
|
|
174
174
|
|
|
175
175
|
// Case and whitespace validation
|
|
176
176
|
Schema.String.check(Schema.isTrimmed()); // No leading/trailing whitespace
|
|
@@ -185,6 +185,16 @@ Schema.String.check(Schema.isBase64());
|
|
|
185
185
|
Schema.String.check(Schema.isBase64Url());
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
For string length, choose the unit deliberately: `isMinLength`, `isMaxLength`,
|
|
189
|
+
and `isBetweenLength` count UTF-16 code units; `isMinCodePoints`,
|
|
190
|
+
`isMaxCodePoints`, and `isBetweenCodePoints` count Unicode code points, not
|
|
191
|
+
grapheme clusters. The latter match JSON Schema length semantics and guide
|
|
192
|
+
native Arbitrary generation. Cardinality bounds must be finite.
|
|
193
|
+
|
|
194
|
+
JSON Schema export can approximate runtime checks. Use the Effect decoder as
|
|
195
|
+
the final authority; see `effect-schema-v4` for exact/approximate check exporters,
|
|
196
|
+
Unicode patterns, and `oneOf` fallback semantics.
|
|
197
|
+
|
|
188
198
|
### Number Filters
|
|
189
199
|
|
|
190
200
|
```typescript
|
|
@@ -220,7 +230,7 @@ import { Schema } from 'effect';
|
|
|
220
230
|
|
|
221
231
|
Schema.Array(Schema.Number).check(Schema.isMinLength(2));
|
|
222
232
|
Schema.Array(Schema.Number).check(Schema.isMaxLength(5));
|
|
223
|
-
Schema.Array(Schema.Number).check(Schema.
|
|
233
|
+
Schema.Array(Schema.Number).check(Schema.isBetweenLength(2, 5));
|
|
224
234
|
```
|
|
225
235
|
|
|
226
236
|
### Combining Multiple Filters
|
|
@@ -329,6 +339,7 @@ const MyForm = Schema.Struct({
|
|
|
329
339
|
|
|
330
340
|
Use `SchemaGetter.checkEffect` for async validation inside a `Schema.decode` transformation:
|
|
331
341
|
|
|
342
|
+
<!-- typecheck -->
|
|
332
343
|
```typescript
|
|
333
344
|
import {
|
|
334
345
|
Effect,
|
|
@@ -345,14 +356,14 @@ async function validateUsername(username: string) {
|
|
|
345
356
|
|
|
346
357
|
const ValidUsername = Schema.String.pipe(
|
|
347
358
|
Schema.decode({
|
|
348
|
-
decode: SchemaGetter.checkEffect((username) =>
|
|
359
|
+
decode: SchemaGetter.checkEffect((username, options) =>
|
|
349
360
|
Effect.promise(() =>
|
|
350
361
|
validateUsername(username).then((valid) =>
|
|
351
362
|
valid
|
|
352
363
|
? undefined
|
|
353
|
-
: new SchemaIssue.InvalidValue(
|
|
354
|
-
|
|
355
|
-
|
|
364
|
+
: new SchemaIssue.InvalidValue(
|
|
365
|
+
{ message: 'Invalid username' }, username, options
|
|
366
|
+
)
|
|
356
367
|
)
|
|
357
368
|
)
|
|
358
369
|
),
|
|
@@ -467,7 +478,7 @@ function split(separator: string) {
|
|
|
467
478
|
|
|
468
479
|
### Schema-derived binary boundaries
|
|
469
480
|
|
|
470
|
-
Use `SchemaBinary.toCodec(schema)` from `effect/
|
|
481
|
+
Use `SchemaBinary.toCodec(schema)` from `effect/encoding` for a compact
|
|
471
482
|
`Uint8Array` representation. It derives the wire layout from the schema's
|
|
472
483
|
**encoded side**, preserving transformations, checks, and decoding/encoding
|
|
473
484
|
services. Use public Schema encode/decode adapters; `toCodecDirect` and the
|
|
@@ -477,7 +488,7 @@ module's internal fast-path functions are not application APIs.
|
|
|
477
488
|
```ts
|
|
478
489
|
import { Effect } from 'effect';
|
|
479
490
|
import * as Schema from 'effect/Schema';
|
|
480
|
-
import { SchemaBinary } from 'effect/
|
|
491
|
+
import { SchemaBinary } from 'effect/encoding';
|
|
481
492
|
|
|
482
493
|
class Reading extends Schema.Class<Reading>('Reading')({
|
|
483
494
|
id: Schema.String,
|
|
@@ -539,6 +550,7 @@ const BooleanFromString = Schema.Literals(['on', 'off']).pipe(
|
|
|
539
550
|
|
|
540
551
|
Use `SchemaTransformation.transformEffect` when transformation might fail:
|
|
541
552
|
|
|
553
|
+
<!-- typecheck -->
|
|
542
554
|
```typescript
|
|
543
555
|
import {
|
|
544
556
|
Effect,
|
|
@@ -551,11 +563,11 @@ import {
|
|
|
551
563
|
|
|
552
564
|
const NumberFromString = Schema.String.pipe(
|
|
553
565
|
Schema.decodeTo(Schema.Number, {
|
|
554
|
-
decode: SchemaGetter.transformEffect((s) =>
|
|
566
|
+
decode: SchemaGetter.transformEffect((s, options) =>
|
|
555
567
|
Option.match(Number.parse(s), {
|
|
556
568
|
onNone: () =>
|
|
557
569
|
Effect.fail(
|
|
558
|
-
new SchemaIssue.InvalidValue(
|
|
570
|
+
new SchemaIssue.InvalidValue({ expected: 'a number' }, s, options)
|
|
559
571
|
),
|
|
560
572
|
onSome: (n) => Effect.succeed(n)
|
|
561
573
|
})
|
|
@@ -945,6 +957,12 @@ const AuthToken = Schema.TemplateLiteralParser(authTemplate.parts);
|
|
|
945
957
|
|
|
946
958
|
### Branded Types
|
|
947
959
|
|
|
960
|
+
Brands are type-only: `Schema.brand` does not add checks or change the runtime
|
|
961
|
+
AST. Pass a single concrete string literal and apply it repeatedly to compose
|
|
962
|
+
brands. `Schema.fromBrand` additionally applies a constructor's checks, and its
|
|
963
|
+
identifier must be that constructor's sole brand key. Representation round trips
|
|
964
|
+
preserve checks, but not nominal brands; reapply brands after rebuilding.
|
|
965
|
+
|
|
948
966
|
```typescript
|
|
949
967
|
import { Schema } from 'effect';
|
|
950
968
|
|
|
@@ -35,7 +35,7 @@ schema helpers so concrete schema operations are retained.
|
|
|
35
35
|
| `typeSchema(schema)` | `toType(schema)` | |
|
|
36
36
|
| `asSchema(schema)` | `revealCodec(schema)` | |
|
|
37
37
|
| `equivalence()` | `toEquivalence()` | |
|
|
38
|
-
| `arbitrary()` | `Arbitrary.schema(schema)` | Native `effect/
|
|
38
|
+
| `arbitrary()` | `Arbitrary.schema(schema)` | Native `effect/Arbitrary`; fast-check bridge removed |
|
|
39
39
|
| `pretty()` | `toFormatter()` | |
|
|
40
40
|
| `parseJson()` | `fromJsonString(Schema.Unknown)` | Public unknown-JSON codec |
|
|
41
41
|
| `parseJson(schema)` | `fromJsonString(schema)` | With-schema version |
|
|
@@ -145,10 +145,35 @@ Schema.Number.check(Schema.isGreaterThan(0));
|
|
|
145
145
|
| `finite` | `isFinite()` |
|
|
146
146
|
| `minLength(n)` | `isMinLength(n)` |
|
|
147
147
|
| `maxLength(n)` | `isMaxLength(n)` |
|
|
148
|
-
| `length(n)` | `
|
|
148
|
+
| `length(n)` | `isBetweenLength(n, n)` |
|
|
149
149
|
| `pattern(regex)` | `isPattern(regex)` |
|
|
150
150
|
| `nonEmptyString` | `isNonEmpty()` |
|
|
151
151
|
|
|
152
|
+
Use `isBetweenLength`, `isBetweenCodePoints`, `isBetweenSize`, and
|
|
153
|
+
`isBetweenProperties` for range checks, and `isStartingWith`, `isEndingWith`,
|
|
154
|
+
and `isIncluding` for string checks. Their `SchemaRepresentation.*Reviver`
|
|
155
|
+
exports and persisted `effect/schema/...` check IDs use these same names;
|
|
156
|
+
update persisted representations as well as source calls.
|
|
157
|
+
|
|
158
|
+
### String length semantics
|
|
159
|
+
|
|
160
|
+
`isMinLength`, `isMaxLength`, and `isBetweenLength` count UTF-16 code units on
|
|
161
|
+
strings and elements on arrays. Use `isMinCodePoints`, `isMaxCodePoints`, and
|
|
162
|
+
`isBetweenCodePoints` for Unicode code point counts, matching JSON Schema length
|
|
163
|
+
keywords. Code points are not grapheme clusters; these checks do not normalize
|
|
164
|
+
strings, and unpaired surrogates each count as one code point. Cardinality bounds
|
|
165
|
+
must be finite; they are rounded down and clamped to zero.
|
|
166
|
+
|
|
167
|
+
<!-- typecheck -->
|
|
168
|
+
```ts
|
|
169
|
+
import * as Schema from 'effect/Schema';
|
|
170
|
+
|
|
171
|
+
const OneCodePoint = Schema.String.check(Schema.isBetweenCodePoints(1, 1));
|
|
172
|
+
const OneCodeUnit = Schema.String.check(Schema.isBetweenLength(1, 1));
|
|
173
|
+
Schema.is(OneCodePoint)('😀'); // true
|
|
174
|
+
Schema.is(OneCodeUnit)('😀'); // false
|
|
175
|
+
```
|
|
176
|
+
|
|
152
177
|
### Removed Filters (no v4 equivalent)
|
|
153
178
|
|
|
154
179
|
`positive`, `negative`, `nonNegative`, `nonPositive` — build these yourself:
|
|
@@ -252,11 +277,11 @@ import {
|
|
|
252
277
|
|
|
253
278
|
const NumberFromString = Schema.String.pipe(
|
|
254
279
|
Schema.decodeTo(Schema.Number, {
|
|
255
|
-
decode: SchemaGetter.transformEffect((s) =>
|
|
280
|
+
decode: SchemaGetter.transformEffect((s, options) =>
|
|
256
281
|
Option.match(Number.parse(s), {
|
|
257
282
|
onNone: () =>
|
|
258
283
|
Effect.fail(
|
|
259
|
-
new SchemaIssue.InvalidValue(
|
|
284
|
+
new SchemaIssue.InvalidValue({ expected: 'a number' }, s, options)
|
|
260
285
|
),
|
|
261
286
|
onSome: (n) => Effect.succeed(n)
|
|
262
287
|
})
|
|
@@ -278,6 +303,35 @@ Schema.Literal(0).transform('a');
|
|
|
278
303
|
Schema.Literals([0, 1]).transform(['a', 'b']);
|
|
279
304
|
```
|
|
280
305
|
|
|
306
|
+
### Brands are type-only
|
|
307
|
+
|
|
308
|
+
`Schema.brand('UserId')` adds a nominal TypeScript distinction, not a runtime
|
|
309
|
+
check or AST annotation. Apply checks before branding. The identifier must be
|
|
310
|
+
one concrete string literal: widened strings, unions, and open template literal
|
|
311
|
+
types are rejected. Compose distinct brands by applying `brand` repeatedly.
|
|
312
|
+
For an enum key, pass the enum member rather than its underlying string value.
|
|
313
|
+
|
|
314
|
+
`Schema.fromBrand(identifier, constructor)` requires the constructor's sole
|
|
315
|
+
concrete brand key and applies its checks. Compose distinct constructors through
|
|
316
|
+
repeated `fromBrand` calls; use `Schema.Union` for alternatives.
|
|
317
|
+
|
|
318
|
+
Representations and generated schema code do not retain type-only brands.
|
|
319
|
+
Reapply branding after rebuilding a schema when the nominal type is required;
|
|
320
|
+
checks supplied by `fromBrand` remain represented. Use an explicit `identifier`
|
|
321
|
+
annotation for reference naming instead of relying on the brand name.
|
|
322
|
+
|
|
323
|
+
<!-- typecheck -->
|
|
324
|
+
```ts
|
|
325
|
+
import * as Schema from 'effect/Schema';
|
|
326
|
+
|
|
327
|
+
const UserId = Schema.NonEmptyString.pipe(Schema.brand('UserId'));
|
|
328
|
+
const TenantUserId = UserId.pipe(Schema.brand('TenantScoped'));
|
|
329
|
+
type TenantUserId = typeof TenantUserId.Type;
|
|
330
|
+
|
|
331
|
+
const id: TenantUserId = Schema.decodeUnknownSync(TenantUserId)('user-1');
|
|
332
|
+
// Both brands are static distinctions; runtime validation is NonEmptyString.
|
|
333
|
+
```
|
|
334
|
+
|
|
281
335
|
## 5. Schema.Data Removal
|
|
282
336
|
|
|
283
337
|
`Schema.Data` is **removed** in v4. No replacement needed — `Equal.equals` now does deep structural comparison on plain objects by default.
|
|
@@ -387,7 +441,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
|
|
|
387
441
|
- `Schema.resolveAnnotationsKey(schema)` returns key-level annotations.
|
|
388
442
|
- `Schema.annotateEncoded({...})` annotates the encoded side of a transformed schema; use `Schema.annotate({...})` for the decoded Type side.
|
|
389
443
|
- Schemas are directly extendable as classes.
|
|
390
|
-
- Derive native generators with `Arbitrary.schema(schema)` from `effect/
|
|
444
|
+
- Derive native generators with `Arbitrary.schema(schema)` from `effect/Arbitrary`. See `effect-testing` for sampling, bounded generation, shrinking, and replay.
|
|
391
445
|
- New built-in schemas:
|
|
392
446
|
- `Schema.DateFromString`
|
|
393
447
|
- `Schema.BigIntFromString`
|
|
@@ -404,7 +458,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
|
|
|
404
458
|
|
|
405
459
|
```ts
|
|
406
460
|
import { Effect, Schema } from 'effect';
|
|
407
|
-
import
|
|
461
|
+
import * as Arbitrary from 'effect/Arbitrary';
|
|
408
462
|
|
|
409
463
|
class UserName extends Schema.NonEmptyString {
|
|
410
464
|
static readonly decodeUnknownSync = Schema.decodeUnknownSync(this);
|
|
@@ -509,13 +563,40 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
|
|
|
509
563
|
|
|
510
564
|
### JSON Schema import, conversion, and Standard Schema
|
|
511
565
|
|
|
566
|
+
- `Schema.toJsonSchemaDocument(schema)` describes the encoded side of
|
|
567
|
+
`Schema.toCodecJson(schema)`. Decode matching JSON through that codec: for
|
|
568
|
+
example, its optional string field accepts JSON `null` as `undefined`, while
|
|
569
|
+
the original `Schema.optional(Schema.String)` rejects `null`.
|
|
570
|
+
- Export is best-effort, and successful JSON Schema validation is not proof that
|
|
571
|
+
Effect decoding will succeed. Known approximate branches cause `oneOf` to
|
|
572
|
+
export as `anyOf`; unions with only exact branches retain `oneOf`. Approximation propagates
|
|
573
|
+
through nested schemas, check dependencies, and recursive references.
|
|
574
|
+
- Custom check `toJsonSchema` callbacks return a fragment for exact semantics,
|
|
575
|
+
`[fragment, true]` for a safe, looser approximation, or `[{}, true]` to omit a
|
|
576
|
+
constraint. The compiler trusts that declaration. Approximate record-key
|
|
577
|
+
patterns cannot select `patternProperties` values; with excess-property mode
|
|
578
|
+
`"error"`, generated `propertyNames` and permissive candidate value schemas
|
|
579
|
+
still leave exact key/value associations to the Effect decoder.
|
|
580
|
+
- String code-unit minima export as `Math.ceil(minimum / 2)` code points, and
|
|
581
|
+
maxima use the same numeric bound but count code points. Code-point checks
|
|
582
|
+
export exact string bounds. Direct `isPattern` exports omit patterns unless
|
|
583
|
+
the regex has `u` and only optional `d`, `g`, or `y` flags; sticky patterns are
|
|
584
|
+
anchored at the start. Casing, safe integers, size, uniqueness, and non-number
|
|
585
|
+
ranges may also have looser or omitted exported constraints.
|
|
512
586
|
- `SchemaRepresentation.fromJsonSchemaDocument` rejects unsupported references,
|
|
513
587
|
validation keywords, object/array `const` or `enum` values, and intersections
|
|
514
588
|
it cannot represent faithfully. Do not discard the failing constraint to make
|
|
515
589
|
an import succeed.
|
|
516
590
|
- A keyword such as `minLength` does not imply `type: 'string'`. Constraints beside
|
|
517
|
-
`const`, `enum`, and `$ref` are applied.
|
|
518
|
-
|
|
591
|
+
`const`, `enum`, and `$ref` are applied. Imports preserve `oneOf` mode; re-export
|
|
592
|
+
follows the approximation rules above. Tuple imports preserve `minItems` even
|
|
593
|
+
when `prefixItems` alone is insufficient.
|
|
594
|
+
- Imports assume a valid Draft 2020-12 document, without meta-schema validation,
|
|
595
|
+
and JSON-compatible instance values. Imported string lengths count code points;
|
|
596
|
+
`patterns: "apply"` compiles in ECMAScript Unicode (`u`) mode and rejects patterns
|
|
597
|
+
invalid in that mode with their source path. Patterns default to rejection;
|
|
598
|
+
ignoring them can also reject valid inputs by creating overlapping `oneOf`
|
|
599
|
+
branches. Import/re-export is not a lossless equivalence guarantee.
|
|
519
600
|
- `JsonSchema` dialect conversion preserves custom keywords and representable
|
|
520
601
|
conditionals, contains, dependencies, identifiers, and tuples; it relocates
|
|
521
602
|
local references and throws for unsupported conversions. These synchronous
|
|
@@ -523,7 +604,7 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
|
|
|
523
604
|
- Import vendored V1 interoperability types from `effect/StandardSchema`, for
|
|
524
605
|
example `StandardSchemaV1` and `StandardJSONSchemaV1`. Continue to adapt Effect
|
|
525
606
|
schemas with `Schema.toStandardSchemaV1`; the new module is not a schema builder.
|
|
526
|
-
- Binary encoding is available from `effect/
|
|
607
|
+
- Binary encoding is available from `effect/encoding` as `SchemaBinary`.
|
|
527
608
|
See `effect-schema-composition` for codecs and `effect-stream` for framing.
|
|
528
609
|
|
|
529
610
|
## Parsing and compilation contracts
|
|
@@ -540,6 +621,9 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
|
|
|
540
621
|
- Declared fields may be inherited and are copied to own output properties;
|
|
541
622
|
dynamic record keys remain own-only and `__proto__` remains own-only. Enforce
|
|
542
623
|
ownership at the boundary when the protocol requires own declared fields.
|
|
624
|
+
Excess-property checks ignore non-enumerable own properties; dynamic string
|
|
625
|
+
and symbol index signatures skip them too. Declare a field explicitly or make
|
|
626
|
+
it enumerable when it must survive decoding or encoding.
|
|
543
627
|
- Class `make`, `makeOption`, and `makeEffect` preserve existing instances. Use
|
|
544
628
|
`new MyClass(fields)` for a distinct instance. Class equivalence now compares
|
|
545
629
|
declared fields, excluding unrelated runtime properties.
|
|
@@ -562,7 +646,7 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
|
|
|
562
646
|
`$id` are rejected; flatten references or explicitly choose to ignore constraints.
|
|
563
647
|
|
|
564
648
|
Experimental JIT/AOT compilation uses the existing `SchemaParser` APIs. Opt in
|
|
565
|
-
globally with `effect/
|
|
649
|
+
globally with `effect/schema/SchemaJITCompiler/enable` or selectively with
|
|
566
650
|
`SchemaJITCompiler.enable(ast)`. AOT's `SchemaAOTCompiler/Build` discovers direct
|
|
567
651
|
schema exports from explicit loaders and writes a self-installing module via
|
|
568
652
|
FileSystem/Path. Choose prepared operations explicitly: omitted operations use
|
|
@@ -51,6 +51,7 @@ The `Scope` object itself:
|
|
|
51
51
|
```ts
|
|
52
52
|
interface Scope {
|
|
53
53
|
readonly strategy: 'sequential' | 'parallel';
|
|
54
|
+
readonly parent: Scope | undefined;
|
|
54
55
|
state: State.Open | State.Closed | State.Empty;
|
|
55
56
|
}
|
|
56
57
|
interface Closeable extends Scope {} // can be passed to Scope.close
|
|
@@ -58,6 +59,11 @@ interface Closeable extends Scope {} // can be passed to Scope.close
|
|
|
58
59
|
|
|
59
60
|
`Scope.Scope` is also the `Context` tag for the current scope, so `yield* Scope.Scope` and `yield* Effect.scope` both return it.
|
|
60
61
|
|
|
62
|
+
`Scope.close` and `Scope.closeUnsafe` require **`Scope.Closeable`**, obtained from
|
|
63
|
+
`Scope.make` / `Scope.fork` (or their unsafe constructors). An ambient
|
|
64
|
+
`Scope.Scope` grants finalizer registration, not authority to close its owner.
|
|
65
|
+
Keep owned scope fields typed `Scope.Closeable`; do not cast a borrowed scope.
|
|
66
|
+
|
|
61
67
|
Imports used throughout this skill:
|
|
62
68
|
|
|
63
69
|
```ts
|
|
@@ -181,7 +187,7 @@ Selective variants: `Effect.onExitIf(self, predicate, f)`, `Effect.onExitFilter(
|
|
|
181
187
|
|
|
182
188
|
Semantics:
|
|
183
189
|
|
|
184
|
-
- Finalizers attached with `onExit`/`ensuring` run in an **uninterruptible region**
|
|
190
|
+
- Finalizers attached with `onExit`/`ensuring` run in an **uninterruptible region** by default. Use public combinators rather than internal runtime primitives.
|
|
185
191
|
- If an `onExit` finalizer fails (`f` may have an error channel `XE`), its error joins the result error channel. If both the source and finalizer fail, their causes are combined rather than one replacing the other.
|
|
186
192
|
- `Effect.ensuring` deliberately requires `Effect<X, never, R1>` as its finalizer, so typed finalizer errors must be handled before attachment. A finalizer defect can still occur at runtime and is combined with an existing source failure.
|
|
187
193
|
- These only fire if the effect **starts** executing.
|
|
@@ -289,8 +295,8 @@ Constructors and operations (all verified against `Scope.ts`):
|
|
|
289
295
|
| `Scope.makeUnsafe` | `(strategy?) => Closeable` | synchronous |
|
|
290
296
|
| `Scope.addFinalizer` | `(scope, finalizer: Effect<unknown>) => Effect<void>` | exit-blind |
|
|
291
297
|
| `Scope.addFinalizerExit` | `(scope, (exit) => Effect<unknown>) => Effect<void>` | exit-aware |
|
|
292
|
-
| `Scope.close` | `(scope, exit: Exit<A, E>) => Effect<void>` | idempotent |
|
|
293
|
-
| `Scope.closeUnsafe` | `(scope, exit) => Effect<void> \| undefined` | low-level; **
|
|
298
|
+
| `Scope.close` | `(scope: Closeable, exit: Exit<A, E>) => Effect<void>` | idempotent; finalizes uninterruptibly by default |
|
|
299
|
+
| `Scope.closeUnsafe` | `(scope: Closeable, exit) => Effect<void> \| undefined` | low-level; **run the returned effect uninterruptibly** or cleanup can be skipped |
|
|
294
300
|
| `Scope.fork` | `(scope, strategy?) => Effect<Closeable>` | child scope, see section 5 |
|
|
295
301
|
| `Scope.forkUnsafe` | `(scope, strategy?) => Closeable` | synchronous |
|
|
296
302
|
| `Scope.provide` / `Scope.use` | dual | see section 3 |
|
|
@@ -299,6 +305,7 @@ Close semantics (from `internal/effect.ts` `scopeCloseFinalizers`):
|
|
|
299
305
|
|
|
300
306
|
- Finalizers run in **reverse registration order** (LIFO).
|
|
301
307
|
- `'sequential'` (default): one at a time, each awaited. `'parallel'`: all started concurrently, then awaited together.
|
|
308
|
+
- `Scope.close` runs finalizers uninterruptibly by default: interruption of the closing fiber waits for cleanup. A finalizer can explicitly restore interruptibility; avoid doing so unless abandoning that cleanup is intentional.
|
|
302
309
|
- **Every finalizer always runs** — a failing finalizer does not prevent the others. All failures are collected and combined into a single `Cause`; `Scope.close` then fails with that combined cause.
|
|
303
310
|
- When scoped work and scope finalization both fail, the work cause and combined finalizer cause are merged.
|
|
304
311
|
- Closing an already-closed scope is a no-op.
|
|
@@ -339,7 +346,7 @@ const program = Effect.gen(function* () {
|
|
|
339
346
|
`Scope.fork(parent)` creates a `Closeable` child registered with the parent:
|
|
340
347
|
|
|
341
348
|
- Closing the **parent** closes the child with the same `Exit`.
|
|
342
|
-
- Closing the **child** first detaches it from the parent
|
|
349
|
+
- Closing the **child** first detaches it from the parent before cleanup runs, even if cleanup throws or is interrupted. The readonly `child.parent` still identifies its parent; it is not a liveness indicator.
|
|
343
350
|
- Forking from an **already-closed** parent returns an already-closed child — finalizers added to it run immediately.
|
|
344
351
|
|
|
345
352
|
This is the splitting primitive: hand part of a lifetime to other code while keeping an upper bound.
|
|
@@ -492,6 +499,7 @@ yield* ScopedRef.set(
|
|
|
492
499
|
Semantics (verified in `ScopedRef.ts` and its tests):
|
|
493
500
|
|
|
494
501
|
- Constructing requires `Scope.Scope`: when the *outer* scope closes, the currently-held value's scope is closed too.
|
|
502
|
+
- The owner also closes in-flight replacement scopes. `make`, `fromAcquire`, and `set` interrupt if their owning scope has closed; they cannot return or install a resource from a closed generation. Replacements retain the reference's original position in the owner's finalizer order.
|
|
495
503
|
- `set` is **synchronized** (internal semaphore — one replacement at a time) and **uninterruptible**.
|
|
496
504
|
- `set` acquires the replacement first. If acquisition fails, its new scope is closed, the error propagates, and the current value remains alive and unchanged.
|
|
497
505
|
- After successful acquisition, `set` closes the old scope before installing the replacement. If the old finalizer defects, the replacement scope is also closed and the reference is not switched, preventing the newly acquired resource from leaking.
|
|
@@ -707,6 +715,6 @@ class Plugin {
|
|
|
707
715
|
11. **Using `acquireUseRelease` and swallowing release errors unknowingly — or the opposite.** Its release *can* fail; a release failure fails the whole effect after successful `use`, and combines with the use cause after failed `use`. Conversely `acquireRelease`'s release is typed `never` — convert errors inside it.
|
|
708
716
|
12. **Acquiring per-request resources in a `Layer.effect` constructor.** Layer finalizers run when the layer scope closes (shutdown), not per call. Acquire per-request resources inside the request handler under `Effect.scoped`, or fork a child scope per item (section 5).
|
|
709
717
|
13. **v3 fork names**: `Effect.fork` → `Effect.forkChild`, `Effect.forkDaemon` → `Effect.forkDetach`. `forkScoped`/`forkIn` keep their names and now accept `{ startImmediately?, uninterruptible? }`.
|
|
710
|
-
14. **Calling `Scope.closeUnsafe` and dropping the result.** It returns `Effect | undefined`;
|
|
718
|
+
14. **Calling `Scope.closeUnsafe` and dropping or interrupting the result.** It requires `Closeable` and returns `Effect | undefined`; run a returned effect uninterruptibly. The scope is already marked closed, so retrying close cannot recover skipped finalizers. Prefer `Scope.close`.
|
|
711
719
|
15. **Expecting interruption to skip cleanup.** Finalizers receive `Exit.failCause` with an interrupt cause and still run (uninterruptibly). Use exit-aware finalizers (`addFinalizer`, `acquireRelease`'s `(a, exit) =>`) to branch on success/failure/interrupt — don't split cleanup across `onError` + success paths.
|
|
712
720
|
16. **Splitting acquire and `Effect.addFinalizer` into separate yields.** Interruption between the two steps leaks the resource. `acquireRelease` wraps acquisition and finalizer registration in a single `uninterruptibleMask` precisely to close this window — use it whenever cleanup is tied to an acquired value.
|
|
@@ -291,7 +291,7 @@ Promote it into its own service when any of these are true:
|
|
|
291
291
|
|
|
292
292
|
```typescript
|
|
293
293
|
// git.ts
|
|
294
|
-
import { ChildProcess, ChildProcessSpawner } from 'effect/
|
|
294
|
+
import { ChildProcess, ChildProcessSpawner } from 'effect/process';
|
|
295
295
|
import { Context, Effect, Layer } from 'effect';
|
|
296
296
|
|
|
297
297
|
export interface Interface {
|