opencode-effect-enforcer 0.2.8 → 0.4.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 +6 -5
- 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 +6 -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-decision-model/SKILL.md +301 -0
- package/skills/effect-ai-decision-model/openrouter.md +70 -0
- package/skills/effect-ai-language-model/SKILL.md +53 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +53 -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
|
@@ -7,8 +7,8 @@ You are an Effect TypeScript expert specializing in the HttpApi module for build
|
|
|
7
7
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
|
-
Use `HttpApi.ParseOptions`
|
|
11
|
-
|
|
10
|
+
Use `HttpApi.ParseOptions` as the fallback for client and server codecs;
|
|
11
|
+
per-slot annotations override it (see below). `HttpApiBuilder.handler` defines a reusable
|
|
12
12
|
endpoint callback with inferred request, success, error, and service types.
|
|
13
13
|
Generated clients and AtomHttpApi calls accept per-call `sseOptions`.
|
|
14
14
|
SSE IDs may be absent: model them with `Schema.optional(Schema.String)`.
|
|
@@ -22,13 +22,13 @@ the generator also accepts OpenAPI 3.2's native `query` operation.
|
|
|
22
22
|
Generated multipart binary fields use `File | Blob` and dedicated `*Multipart`
|
|
23
23
|
component exports. Keep generated imports aligned with the chosen transport.
|
|
24
24
|
|
|
25
|
-
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
25
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Inspect the `effect@4.0.0` tag for this skill; main may be newer. These APIs remain `@stability unstable` and may break in minor releases despite their shorter import paths. Keep `effect` and companion packages on the same release.
|
|
26
26
|
|
|
27
27
|
Key reference files:
|
|
28
28
|
|
|
29
29
|
- `packages/effect/HTTPAPI.md` — canonical HttpApi documentation
|
|
30
|
-
- `packages/effect/src/
|
|
31
|
-
- `packages/effect/typetest/
|
|
30
|
+
- `packages/effect/src/http-api/*.ts` — module sources
|
|
31
|
+
- `packages/effect/typetest/http-api/*.tst.ts` — type-level contracts
|
|
32
32
|
- `packages/platform/node/test/HttpApi.test.ts` — comprehensive runtime tests
|
|
33
33
|
- `ai-docs/src/51_http-server/` — server walkthrough with fixtures
|
|
34
34
|
- `ai-docs/src/50_http-client/` — HttpClient walkthrough
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
HttpApiSwagger,
|
|
52
52
|
HttpApiTest,
|
|
53
53
|
OpenApi
|
|
54
|
-
} from 'effect/
|
|
54
|
+
} from 'effect/http-api';
|
|
55
55
|
|
|
56
56
|
// HTTP primitives (router, server, client, multipart)
|
|
57
57
|
import {
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
HttpServerResponse,
|
|
67
67
|
HttpStatus,
|
|
68
68
|
Multipart
|
|
69
|
-
} from 'effect/
|
|
69
|
+
} from 'effect/http';
|
|
70
70
|
|
|
71
71
|
// Platform server (Node.js — Bun has @effect/platform-bun/BunHttpServer)
|
|
72
72
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
@@ -103,12 +103,14 @@ HttpApiEndpoint.delete('name', '/path', { ... });
|
|
|
103
103
|
HttpApiEndpoint.head('name', '/path', { ... });
|
|
104
104
|
HttpApiEndpoint.options('name', '/path', { ... });
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
HttpApiEndpoint.query('name', '/path', { ... });
|
|
107
|
+
|
|
108
|
+
// Generic factory for a supported HttpMethod
|
|
109
|
+
const query = HttpApiEndpoint.make('QUERY');
|
|
110
|
+
query('name', '/path', { ... });
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
The first argument is the endpoint name (used as the method name in the generated client). The second is the route path. The third is an options object with schemas. For methods with no body (`
|
|
113
|
+
The first argument is the endpoint name (used as the method name in the generated client). The second is the route path. The third is an options object with schemas. For methods with no body (`GET`, `HEAD`, `OPTIONS`, `TRACE`), the `payload` option is treated as a query-string-encoded record of fields. `DELETE` and `QUERY` can carry bodies; `make` accepts the closed `HttpMethod` union, not arbitrary method strings.
|
|
112
114
|
|
|
113
115
|
### Endpoint Options
|
|
114
116
|
|
|
@@ -258,6 +260,42 @@ Group- and API-level prefixing are described below.
|
|
|
258
260
|
|
|
259
261
|
## Schema Annotations
|
|
260
262
|
|
|
263
|
+
### Per-slot parse options
|
|
264
|
+
|
|
265
|
+
Annotate the API, group, or endpoint with `HttpApi.ParamsParseOptions`,
|
|
266
|
+
`QueryParseOptions`, `HeadersParseOptions`, `PayloadParseOptions`,
|
|
267
|
+
`SuccessParseOptions`, or `ErrorParseOptions`. Each controls both sides of that
|
|
268
|
+
codec: server decoding/client encoding for inputs, the reverse for outputs.
|
|
269
|
+
Success options cover streamed/SSE bodies too; header options cover request
|
|
270
|
+
headers and `WithHeaders` response headers independently of their bodies.
|
|
271
|
+
|
|
272
|
+
For the same annotation, endpoint overrides group, which overrides API. A slot
|
|
273
|
+
annotation at **any** level wins over fallback `ParseOptions` at **every** level.
|
|
274
|
+
Objects replace rather than merge. Annotate before constructing handler groups.
|
|
275
|
+
Real HTTP headers include transport fields: strict fallback parsing otherwise
|
|
276
|
+
rejects them, while `onExcessProperty: 'preserve'` retains them in decoded values.
|
|
277
|
+
Use an explicit empty header override to retain Schema defaults:
|
|
278
|
+
|
|
279
|
+
<!-- typecheck -->
|
|
280
|
+
```ts
|
|
281
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api';
|
|
282
|
+
import * as Schema from 'effect/Schema';
|
|
283
|
+
|
|
284
|
+
class CreateUser extends Schema.Class<CreateUser>('CreateUser')({
|
|
285
|
+
name: Schema.String
|
|
286
|
+
}) {}
|
|
287
|
+
|
|
288
|
+
const Api = HttpApi.make('StrictApi').add(
|
|
289
|
+
HttpApiGroup.make('users').add(
|
|
290
|
+
HttpApiEndpoint.post('create', '/users', {
|
|
291
|
+
headers: { 'x-api-key': Schema.String },
|
|
292
|
+
payload: CreateUser
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
).annotate(HttpApi.ParseOptions, { onExcessProperty: 'error' })
|
|
296
|
+
.annotate(HttpApi.HeadersParseOptions, {});
|
|
297
|
+
```
|
|
298
|
+
|
|
261
299
|
### Status Codes
|
|
262
300
|
|
|
263
301
|
```ts
|
|
@@ -281,7 +319,7 @@ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
|
281
319
|
|
|
282
320
|
`HttpApiSchema.StatusLiteral` is the exported keyof type for the literal form. The full set covers the standard codes (`Continue`, `OK`, `Created`, `Accepted`, `NoContent`, `MovedPermanently`, `Found`, `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `MethodNotAllowed`, `NotAcceptable`, `RequestTimeout`, `Conflict`, `Gone`, `UnprocessableEntity`, `TooManyRequests`, `InternalServerError`, `NotImplemented`, `BadGateway`, `ServiceUnavailable`, `GatewayTimeout`, etc.). Unannotated success schemas default to 200, and unannotated error schemas default to 500. If you omit `success`, the endpoint defaults to `HttpApiSchema.NoContent` (204). `success: Schema.Void` is an empty 200 response unless you annotate it or use `HttpApiSchema.NoContent`.
|
|
283
321
|
|
|
284
|
-
The literal mapping is centralized in `HttpStatus` from `effect/
|
|
322
|
+
The literal mapping is centralized in `HttpStatus` from `effect/http`. Use `HttpStatus.fromLiteral` when plain HTTP code needs the corresponding numeric literal type; `HttpApiSchema.status` uses the same mapping internally:
|
|
285
323
|
|
|
286
324
|
```ts
|
|
287
325
|
HttpStatus.fromLiteral('OK'); // 200
|
|
@@ -638,7 +676,7 @@ const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute);
|
|
|
638
676
|
If you forget to provide a group's handler layer you'll get a clear runtime defect:
|
|
639
677
|
|
|
640
678
|
```
|
|
641
|
-
HttpApiGroup "users" not found (key: "effect/
|
|
679
|
+
HttpApiGroup "users" not found (key: "effect/http-api/HttpApiGroup/users").
|
|
642
680
|
Did you forget to provide HttpApiBuilder.group(api, "users", ...)?
|
|
643
681
|
Available groups: <list>
|
|
644
682
|
```
|
|
@@ -668,6 +706,12 @@ export const { handler, dispose } = HttpRouter.toWebHandler(
|
|
|
668
706
|
|
|
669
707
|
`HttpRouter.serve` and `HttpRouter.toWebHandler` both also accept `routerConfig` (passed to find-my-way) and `middleware` (a wrap function applied to the entire HTTP server pipeline).
|
|
670
708
|
|
|
709
|
+
`HttpRouter.serve` and `toHttpEffect` build with a fresh router in a forked layer
|
|
710
|
+
memo map. Pass registration layers directly to them; do not pre-provide
|
|
711
|
+
`HttpRouter.layer` to `ApiRoutes`. Services first built inside these entrypoints
|
|
712
|
+
are private; provide services shared with siblings outside them. `toWebHandler`
|
|
713
|
+
owns a separate build by default, but an explicit `memoMap` is used as supplied.
|
|
714
|
+
|
|
671
715
|
> There is no `HttpApiBuilder.toWebHandler` — always go through `HttpRouter.toWebHandler` (or `HttpRouter.serve` for a long-running server).
|
|
672
716
|
|
|
673
717
|
## Errors
|
|
@@ -1240,6 +1284,12 @@ const spec = OpenApi.fromApi(Api);
|
|
|
1240
1284
|
// spec is OpenAPI 3.1.0
|
|
1241
1285
|
```
|
|
1242
1286
|
|
|
1287
|
+
Parameters and response headers are documented from their **encoded** object
|
|
1288
|
+
representation, preserving property annotations. Decode-defaulted optional keys
|
|
1289
|
+
remain optional on the wire (path parameters still must be required by OpenAPI).
|
|
1290
|
+
Text bodies preserve encoded string literals and exportable checks such as
|
|
1291
|
+
patterns, including when opaque schemas or component references are involved.
|
|
1292
|
+
|
|
1243
1293
|
`fromApi` caches by both the `HttpApi` instance and the identity of the options object, but every call returns a fresh mutable spec copy. Mutating one returned spec does not contaminate later calls. Reuse one immutable options object to reuse the cache; mutating that options object does not invalidate an existing entry. The clone preserves frozen `JSON.rawJSON` values rather than flattening them into ordinary objects.
|
|
1244
1294
|
|
|
1245
1295
|
The options accept the Schema representation `referencePolicy`. It runs at the canonical JSON-encoded AST boundary and controls which schemas become OpenAPI component references; by default only schemas with resolved identifiers are extracted, while anonymous non-recursive schemas remain inline:
|
|
@@ -1420,7 +1470,7 @@ For multipart streaming, see `HttpApiSchema.asMultipartStream` above.
|
|
|
1420
1470
|
`HttpApiTest.groups(api, groupNames, { baseUrl? })` builds a fully typed `HttpApiClient` that runs against your real handler layers in memory — no HTTP server, no port. List the groups whose handlers you want to exercise; all other groups are auto-stubbed with `Effect.die`. The default `baseUrl` is `http://localhost:3000`; pass `{ baseUrl }` when tests rely on URL construction.
|
|
1421
1471
|
|
|
1422
1472
|
```ts
|
|
1423
|
-
import { HttpApiTest } from 'effect/
|
|
1473
|
+
import { HttpApiTest } from 'effect/http-api';
|
|
1424
1474
|
import { NodeHttpServer } from '@effect/platform-node';
|
|
1425
1475
|
import { Effect, Layer } from 'effect';
|
|
1426
1476
|
import { it } from '@effect/vitest';
|
|
@@ -1467,7 +1517,7 @@ it.effect('GET /users responds 200', () =>
|
|
|
1467
1517
|
|
|
1468
1518
|
## Reactive Integration
|
|
1469
1519
|
|
|
1470
|
-
For React/Atom-driven UIs, `effect/
|
|
1520
|
+
For React/Atom-driven UIs, `effect/reactivity/AtomHttpApi` builds a service that exposes typed `query` and `mutation` atoms generated from an HttpApi:
|
|
1471
1521
|
|
|
1472
1522
|
```ts
|
|
1473
1523
|
class ApiAtom extends AtomHttpApi.Service<ApiAtom>()('app/ApiAtom', {
|
|
@@ -1499,7 +1549,7 @@ import {
|
|
|
1499
1549
|
HttpClient,
|
|
1500
1550
|
HttpClientRequest,
|
|
1501
1551
|
HttpClientResponse
|
|
1502
|
-
} from 'effect/
|
|
1552
|
+
} from 'effect/http';
|
|
1503
1553
|
|
|
1504
1554
|
class Todo extends Schema.Class<Todo>('Todo')({
|
|
1505
1555
|
userId: Schema.Number,
|
|
@@ -1573,7 +1623,7 @@ export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
|
1573
1623
|
|
|
1574
1624
|
// --- api/Authorization.ts ---
|
|
1575
1625
|
import { Context, Schema } from 'effect';
|
|
1576
|
-
import { HttpApiMiddleware, HttpApiSecurity } from 'effect/
|
|
1626
|
+
import { HttpApiMiddleware, HttpApiSecurity } from 'effect/http-api';
|
|
1577
1627
|
|
|
1578
1628
|
export class CurrentUser extends Context.Service<CurrentUser, User>()(
|
|
1579
1629
|
'app/CurrentUser'
|
|
@@ -1590,7 +1640,7 @@ export class Authorization extends HttpApiMiddleware.Service<
|
|
|
1590
1640
|
|
|
1591
1641
|
// --- api/Users.ts ---
|
|
1592
1642
|
import { Schema } from 'effect';
|
|
1593
|
-
import { HttpApiEndpoint, HttpApiGroup } from 'effect/
|
|
1643
|
+
import { HttpApiEndpoint, HttpApiGroup } from 'effect/http-api';
|
|
1594
1644
|
|
|
1595
1645
|
export class UsersApi extends HttpApiGroup.make('users')
|
|
1596
1646
|
.add(
|
|
@@ -1623,7 +1673,7 @@ export class SystemApi extends HttpApiGroup.make('system', {
|
|
|
1623
1673
|
) {}
|
|
1624
1674
|
|
|
1625
1675
|
// --- api/Api.ts ---
|
|
1626
|
-
import { HttpApi, OpenApi } from 'effect/
|
|
1676
|
+
import { HttpApi, OpenApi } from 'effect/http-api';
|
|
1627
1677
|
|
|
1628
1678
|
export class Api extends HttpApi.make('app')
|
|
1629
1679
|
.add(UsersApi)
|
|
@@ -1663,7 +1713,7 @@ export const AuthorizationLayer = Layer.effect(
|
|
|
1663
1713
|
|
|
1664
1714
|
// --- server/users.ts ---
|
|
1665
1715
|
import { Effect, Layer } from 'effect';
|
|
1666
|
-
import { HttpApiBuilder } from 'effect/
|
|
1716
|
+
import { HttpApiBuilder } from 'effect/http-api';
|
|
1667
1717
|
|
|
1668
1718
|
export const UsersHandlers = HttpApiBuilder.group(
|
|
1669
1719
|
Api,
|
|
@@ -1692,8 +1742,8 @@ export const SystemHandlers = HttpApiBuilder.group(
|
|
|
1692
1742
|
// --- server/main.ts ---
|
|
1693
1743
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
1694
1744
|
import { Layer } from 'effect';
|
|
1695
|
-
import { HttpRouter } from 'effect/
|
|
1696
|
-
import { HttpApiBuilder, HttpApiScalar } from 'effect/
|
|
1745
|
+
import { HttpRouter } from 'effect/http';
|
|
1746
|
+
import { HttpApiBuilder, HttpApiScalar } from 'effect/http-api';
|
|
1697
1747
|
import { createServer } from 'node:http';
|
|
1698
1748
|
|
|
1699
1749
|
const ApiRoutes = HttpApiBuilder.layer(Api, {
|
|
@@ -3,9 +3,9 @@ name: effect-http-client
|
|
|
3
3
|
description: Make outgoing HTTP requests with Effect's HttpClient — HttpClientRequest builders, schema-decoded HttpClientResponse bodies, the HttpClientError taxonomy, retryTransient/rate limiting/cookies/redirects, streaming uploads and downloads, and FetchHttpClient/NodeHttpClient transport layers. Use when calling external REST/JSON APIs, uploading or downloading files and streams, adding retries/auth/tracing to outbound HTTP, or mocking HTTP responses in tests.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in outgoing HTTP with `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in outgoing HTTP with `effect/http` (`HttpClient`, `HttpClientRequest`, `HttpClientResponse`).
|
|
7
7
|
|
|
8
|
-
In v4 there is no `@effect/platform` package — the HTTP client lives in the `effect` package under `effect/
|
|
8
|
+
In v4 there is no `@effect/platform` package — the HTTP client lives in the `effect` package under `effect/http`. Only the platform transports (`NodeHttpClient`, `BunHttpClient`) live in `@effect/platform-*` packages.
|
|
9
9
|
|
|
10
10
|
## Effect Source Reference
|
|
11
11
|
|
|
@@ -18,23 +18,23 @@ for HTTP value schemas. Import Undici-specific APIs from
|
|
|
18
18
|
`@effect/platform-node/Undici`; the transport layer owns their initialization.
|
|
19
19
|
HTTP `QUERY` is supported; configure CORS `allowedMethods` explicitly if needed.
|
|
20
20
|
|
|
21
|
-
The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read
|
|
21
|
+
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. APIs tagged `@stability unstable`, including these HTTP modules and Undici integration, may break in minor releases. Keep Effect-family packages on the same release.
|
|
22
22
|
|
|
23
23
|
Key files:
|
|
24
24
|
|
|
25
|
-
- `packages/effect/src/
|
|
26
|
-
- `packages/effect/src/
|
|
27
|
-
- `packages/effect/src/
|
|
28
|
-
- `packages/effect/src/
|
|
29
|
-
- `packages/effect/src/
|
|
30
|
-
- `packages/effect/src/
|
|
31
|
-
- `packages/effect/src/
|
|
32
|
-
- `packages/effect/src/
|
|
33
|
-
- `packages/effect/src/
|
|
34
|
-
- `packages/effect/src/
|
|
35
|
-
- `packages/effect/src/
|
|
25
|
+
- `packages/effect/src/http/HttpClient.ts` — the `HttpClient` service, `make`/`makeWith`, every client combinator (`mapRequest`, `transform`, `filterStatus*`, `retry`, `retryTransient`, `withRateLimiter`, `withCookiesRef`, `withScope`, `followRedirects`, `catch*`, `tap*`), tracing references
|
|
26
|
+
- `packages/effect/src/http/HttpClientRequest.ts` — immutable request model, method constructors, URL/param/header/body combinators, `toWeb`/`fromWeb`
|
|
27
|
+
- `packages/effect/src/http/HttpClientResponse.ts` — response model, `schemaJson`/`schemaNoBody`, `matchStatus`, `filterStatus(Ok)`, `stream`
|
|
28
|
+
- `packages/effect/src/http/HttpIncomingMessage.ts` — shared body accessors plus `JsonOptions`, `schemaBodyJson`, `schemaBodyUrlParams`, `schemaHeaders` (re-exported by HttpClientResponse)
|
|
29
|
+
- `packages/effect/src/http/HttpClientError.ts` — `HttpClientError` wrapper and its `reason` union
|
|
30
|
+
- `packages/effect/src/http/HttpBody.ts` — body variants (`Empty`, `Raw`, `Uint8Array`, `FormData`, `Stream`) and constructors (`json`, `jsonSchema`, `text`, `urlParams`, `formDataRecord`, `stream`, `file`)
|
|
31
|
+
- `packages/effect/src/http/FetchHttpClient.ts` — fetch transport: `layer`, `Fetch` reference, `RequestInit` service
|
|
32
|
+
- `packages/effect/src/http/UrlParams.ts` — ordered query-param model, coercion rules, schemas
|
|
33
|
+
- `packages/effect/src/http/Url.ts` — immutable helpers over the native `URL`
|
|
34
|
+
- `packages/effect/src/http/Cookies.ts` — cookie model, `fromSetCookie`, `toCookieHeader`, `getValue`
|
|
35
|
+
- `packages/effect/src/http/Headers.ts` — header model, `Input` forms, `CurrentRedactedNames`
|
|
36
36
|
- `packages/platform/node/src/NodeHttpClient.ts` — Node transports: undici, node:http, fetch re-export
|
|
37
|
-
- `packages/effect/test/
|
|
37
|
+
- `packages/effect/test/http/HttpClient.test.ts` — retryTransient, withRateLimiter, abort semantics
|
|
38
38
|
- `ai-docs/src/50_http-client/10_basics.ts` — canonical "wrap a configured client in a service" lesson
|
|
39
39
|
|
|
40
40
|
## Core Model
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
HttpClientRequest,
|
|
70
70
|
HttpClientResponse,
|
|
71
71
|
UrlParams
|
|
72
|
-
} from 'effect/
|
|
72
|
+
} from 'effect/http';
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
For Node-specific transports:
|
|
@@ -215,7 +215,7 @@ HttpClientRequest.trace('/debug');
|
|
|
215
215
|
HttpClientRequest.get('/search', { urlParams: { q: 'effect' }, acceptJson: true });
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
These come from the generic factory `HttpClientRequest.make(method)`; `HttpClientRequest.setMethod` swaps the method on an existing request. `HttpMethod` is a closed union of
|
|
218
|
+
These come from the generic factory `HttpClientRequest.make(method)`; `HttpClientRequest.setMethod` swaps the method on an existing request. `HttpMethod` is a closed union of nine verbs, including body-carrying `QUERY` (`HttpClientRequest.query`, `client.query`, `HttpClient.query`) — there is no path for custom methods like `REPORT`.
|
|
219
219
|
|
|
220
220
|
### URL combinators
|
|
221
221
|
|
|
@@ -442,6 +442,10 @@ const lines = HttpClientResponse.stream(client.get('/logs')).pipe(
|
|
|
442
442
|
|
|
443
443
|
Every client failure is a single tagged error, `HttpClientError`, wrapping a `reason` union:
|
|
444
444
|
|
|
445
|
+
For unknown boundary values, use the module guards rather than inspecting private
|
|
446
|
+
type IDs. Body construction failures have `HttpBody.isHttpBodyError`; cookie
|
|
447
|
+
construction failures have `Cookies.isCookiesError`.
|
|
448
|
+
|
|
445
449
|
| `reason._tag` | When | Has `response`? |
|
|
446
450
|
|---|---|---|
|
|
447
451
|
| `TransportError` | network/connection failure while sending | no |
|
|
@@ -620,12 +624,12 @@ The undici transport neutralizes undici's own timeouts (`headersTimeout` one hou
|
|
|
620
624
|
|
|
621
625
|
### `HttpClient.withRateLimiter`
|
|
622
626
|
|
|
623
|
-
Client-side rate limiting backed by the `RateLimiter` service from `effect/
|
|
627
|
+
Client-side rate limiting backed by the `RateLimiter` service from `effect/persistence`. It delays requests past the limit, **automatically retries 429s** (responses or `StatusCodeError`s) back through the limiter honoring `retry-after`, and by default updates its limit/window from `ratelimit-*` / `x-ratelimit-*` response headers.
|
|
624
628
|
|
|
625
629
|
Because `withRateLimiter` retries 429 responses independently of the HTTP method, apply it with `times > 0` only to a client restricted to proven-idempotent operations. For a mixed or non-idempotent client, set `times: 0` and handle the returned 429 as a visible typed failure.
|
|
626
630
|
|
|
627
631
|
```ts
|
|
628
|
-
import { RateLimiter } from 'effect/
|
|
632
|
+
import { RateLimiter } from 'effect/persistence';
|
|
629
633
|
|
|
630
634
|
const limitedReads = Effect.gen(function* () {
|
|
631
635
|
const limiter = yield* RateLimiter.RateLimiter;
|
|
@@ -828,7 +832,7 @@ For declarative API clients derived from an `HttpApi` definition, see the effect
|
|
|
828
832
|
|
|
829
833
|
```ts
|
|
830
834
|
import { Context, Effect, flow, Layer, Schedule, Schema } from 'effect';
|
|
831
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from 'effect/
|
|
835
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from 'effect/http';
|
|
832
836
|
|
|
833
837
|
class Todo extends Schema.Class<Todo>('Todo')({
|
|
834
838
|
userId: Schema.Number,
|
|
@@ -978,7 +982,7 @@ const makeGithubReads = Effect.gen(function* () {
|
|
|
978
982
|
|
|
979
983
|
## Common Mistakes
|
|
980
984
|
|
|
981
|
-
1. **v3 imports** — `@effect/platform/HttpClient` and friends no longer exist. Import `HttpClient`, `HttpClientRequest`, `HttpClientResponse`, `FetchHttpClient`, etc. from `effect/
|
|
985
|
+
1. **v3 imports** — `@effect/platform/HttpClient` and friends no longer exist. Import `HttpClient`, `HttpClientRequest`, `HttpClientResponse`, `FetchHttpClient`, etc. from `effect/http`; only `NodeHttpClient` comes from `@effect/platform-node`.
|
|
982
986
|
2. **Calling body accessors as methods** — `response.text`, `response.json`, `response.stream` are property getters returning Effects/Streams. `yield* response.json`, not `await response.json()`.
|
|
983
987
|
3. **`HttpClientRequest.del` does not exist** — the request constructor is `HttpClientRequest.delete`; the client/service method and accessor are `client.del` / `HttpClient.del`.
|
|
984
988
|
4. **Catching v3 error tags** — there are no top-level `RequestError`/`ResponseError` tags anymore. Everything is one tag, `HttpClientError`; branch on `error.reason._tag` (`TransportError`, `StatusCodeError`, `DecodeError`, ...).
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: effect-http-server
|
|
3
|
-
description: Build HTTP servers with effect/
|
|
3
|
+
description: Build HTTP servers with effect/http — HttpRouter routes and middleware, HttpServerRequest schema decoding, HttpServerResponse constructors, multipart uploads, websocket upgrades, static files, NodeHttpServer/BunHttpServer layers, and in-memory web handlers. Use when serving raw HTTP routes, reading request bodies/cookies/uploads, writing server middleware, streaming responses, or testing handlers without a real port.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in HTTP servers built with `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in HTTP servers built with `effect/http` — `HttpRouter`, `HttpServer`, `HttpServerRequest`, `HttpServerResponse`, `HttpMiddleware`, and the platform server layers.
|
|
7
7
|
|
|
8
8
|
This skill covers the imperative HTTP server primitives. For the declarative, schema-first `HttpApi`/OpenAPI layer see the `effect-http-api` skill; for HTTP clients see `effect-http-client`; for raw TCP/WebSocket sockets see `effect-socket`.
|
|
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 modules 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/
|
|
25
|
-
- `packages/effect/src/
|
|
26
|
-
- `packages/effect/src/
|
|
16
|
+
- `packages/effect/src/http/HttpRouter.ts` — router service, `add`/`addAll`/`route`/`use`, `serve`, `toWebHandler`, schema decoders, `middleware`, `cors`, `provideRequest`, `RouterConfig`
|
|
17
|
+
- `packages/effect/src/http/HttpServer.ts` — `HttpServer` service, `serve`/`serveEffect`, address helpers, `layerTestClient`, `layerServices`
|
|
18
|
+
- `packages/effect/src/http/HttpServerRequest.ts` — request model, body accessors, `schemaBodyJson`/`schemaBodyForm`/etc., `ParsedSearchParams`, `upgrade`, `MaxBodySize`
|
|
19
|
+
- `packages/effect/src/http/HttpServerResponse.ts` — every response constructor and combinator, `toWeb`/`fromWeb`
|
|
20
|
+
- `packages/effect/src/http/HttpMiddleware.ts` — `logger`, `tracer`, `cors`, `xForwardedHeaders`, `searchParamsParser`, tracing config references
|
|
21
|
+
- `packages/effect/src/http/HttpEffect.ts` — `toWebHandler*`, `fromWebHandler`, `toHandled`, pre-response handlers, request scope management
|
|
22
|
+
- `packages/effect/src/http/HttpServerError.ts` — `HttpServerError` + reasons, `causeResponse`, `ClientAbort`
|
|
23
|
+
- `packages/effect/src/http/HttpServerRespondable.ts` — the error-to-response protocol
|
|
24
|
+
- `packages/effect/src/http/HttpBody.ts` — body variants (`Empty`/`Raw`/`Uint8Array`/`FormData`/`Stream`) and constructors
|
|
25
|
+
- `packages/effect/src/http/Headers.ts`, `Cookies.ts`, `Multipart.ts` — header/cookie/multipart models and limits
|
|
26
|
+
- `packages/effect/src/http/HttpStaticServer.ts` — static file serving
|
|
27
27
|
- `packages/platform/node/src/NodeHttpServer.ts` — Node server adapter, `layer`, `layerTest`, graceful shutdown
|
|
28
28
|
- `packages/platform/bun/src/BunHttpServer.ts` — Bun equivalent
|
|
29
29
|
- `packages/platform/node/test/NodeHttpServer.test.ts` — the best end-to-end reference for real route/middleware/multipart wiring
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
HttpStatus,
|
|
56
56
|
HttpStaticServer,
|
|
57
57
|
Multipart
|
|
58
|
-
} from 'effect/
|
|
58
|
+
} from 'effect/http';
|
|
59
59
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
60
60
|
import { createServer } from 'node:http';
|
|
61
61
|
```
|
|
@@ -86,7 +86,7 @@ Layer.launch(Main).pipe(NodeRuntime.runMain);
|
|
|
86
86
|
HttpRouter.add(method, path, handler, options?): Layer<never, never, HttpRouter | ...>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
- `method`: `
|
|
89
|
+
- `method`: `HttpMethod | '*'`, including `HEAD`, `TRACE`, and body-carrying `QUERY` (`'*'` matches all methods; `HEAD` requests automatically fall back to the matching `GET` route with the body stripped)
|
|
90
90
|
- `path`: `PathInput` — must start with `/`, or be `*`. `:name` captures a path param; a trailing `/*` is a wildcard (and also matches the bare prefix: `'/files/*'` matches `/files` too)
|
|
91
91
|
- `options`: `{ uninterruptible?: boolean }` — handlers are interruptible by default (client disconnect interrupts the fiber); set `true` for must-complete handlers
|
|
92
92
|
|
|
@@ -159,7 +159,7 @@ const { id: pathOnly } = yield* HttpRouter.schemaPathParams(IdParams); // path p
|
|
|
159
159
|
|
|
160
160
|
### Router configuration
|
|
161
161
|
|
|
162
|
-
The matcher is `
|
|
162
|
+
The matcher is the bundled `FindMyWay` module. Configure via the `RouterConfig` reference or the `routerConfig` option of `serve`/`toWebHandler`:
|
|
163
163
|
|
|
164
164
|
```ts
|
|
165
165
|
Layer.succeed(HttpRouter.RouterConfig)({
|
|
@@ -170,6 +170,9 @@ Layer.succeed(HttpRouter.RouterConfig)({
|
|
|
170
170
|
});
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
Route parameter construction does not require string code generation (`new
|
|
174
|
+
Function`), so routing works under CSP restrictions on dynamic code evaluation.
|
|
175
|
+
|
|
173
176
|
---
|
|
174
177
|
|
|
175
178
|
## 2. Reading the Request
|
|
@@ -310,6 +313,11 @@ handler.pipe(
|
|
|
310
313
|
|
|
311
314
|
The Effect v4 parser stops consuming input as soon as a part-count, part-size, or field-size limit is exceeded. Active file-part streams are terminated with the multipart failure when a limit is exceeded or the body ends unexpectedly, so consumers fail promptly instead of hanging. Keep consuming or supervising every exposed file stream so that failure is observed.
|
|
312
315
|
|
|
316
|
+
Buffered parts produced while a file is being read are emitted before pulling
|
|
317
|
+
more input. Use `Multipart.isMultipartError` for unknown boundary failures;
|
|
318
|
+
`HttpBody.isHttpBodyError` and `Cookies.isCookiesError` guard their corresponding
|
|
319
|
+
construction errors.
|
|
320
|
+
|
|
313
321
|
---
|
|
314
322
|
|
|
315
323
|
## 4. Building Responses
|
|
@@ -585,6 +593,17 @@ yield* HttpEffect.appendPreResponseHandler((request, response) =>
|
|
|
585
593
|
|
|
586
594
|
## 7. Serving
|
|
587
595
|
|
|
596
|
+
`HttpRouter.serve` and `toHttpEffect` isolate entrypoints with a fresh router and
|
|
597
|
+
forked layer memo map. Reusing an app layer in two such entrypoints builds it
|
|
598
|
+
separately; routes and global middleware do not leak between them. Layers first
|
|
599
|
+
built inside an app are private to that entrypoint. Build/provide services that
|
|
600
|
+
siblings must share outside `serve`/`toHttpEffect`. `toWebHandler` has a separate
|
|
601
|
+
build by default, but its explicit `memoMap` option is passed through as supplied;
|
|
602
|
+
sharing that map intentionally shares memoized layers, including the router.
|
|
603
|
+
Pass route registration layers to each entrypoint with their `HttpRouter`
|
|
604
|
+
requirement intact; pre-providing `HttpRouter.layer` registers on a different
|
|
605
|
+
router and leaves the served router empty.
|
|
606
|
+
|
|
588
607
|
### Node
|
|
589
608
|
|
|
590
609
|
```ts
|
|
@@ -654,6 +673,14 @@ yield* HttpServer.serveEffect(httpEffect);
|
|
|
654
673
|
|
|
655
674
|
## 8. WebSocket Upgrades
|
|
656
675
|
|
|
676
|
+
Node and Bun HTTP upgrade adapters close server WebSockets according to the
|
|
677
|
+
owning scope's exit: `1000` on success, `1001` for interruption-only failure, and
|
|
678
|
+
`1011` for other failures (including mixed failure/interruption causes). Explicit
|
|
679
|
+
close codes already sent by the application are preserved. HTTP error-to-response
|
|
680
|
+
handling and observation middleware retain the original handler failure for
|
|
681
|
+
request-scope finalizers; mapping an error to an HTTP response does not turn that
|
|
682
|
+
scope exit into success.
|
|
683
|
+
|
|
657
684
|
In `@effect/platform-bun`, outgoing WebSocket messages are compressed when
|
|
658
685
|
per-message deflate is configured **and negotiated**. The server option
|
|
659
686
|
`websocket.compressionThreshold` sets the minimum byte size (default `1024`);
|
|
@@ -661,7 +688,7 @@ smaller messages stay uncompressed. Configure it alongside
|
|
|
661
688
|
`websocket.perMessageDeflate` on `BunHttpServer.layer` rather than pre-compressing
|
|
662
689
|
application payloads. See `packages/platform/bun/src/BunHttpServer.ts`.
|
|
663
690
|
|
|
664
|
-
`request.upgrade` yields a `Socket` (from `effect/
|
|
691
|
+
`request.upgrade` yields a `Socket` (from `effect/socket`) once the connection is upgraded. Both `NodeHttpServer` and `BunHttpServer` handle the platform `upgrade` events for you — just write a normal route:
|
|
665
692
|
|
|
666
693
|
```ts
|
|
667
694
|
const WsRoute = HttpRouter.add('GET', '/ws', Effect.gen(function* () {
|
|
@@ -755,6 +782,9 @@ const { handler: appHandler, dispose: disposeApp } = HttpRouter.toWebHandler(
|
|
|
755
782
|
|
|
756
783
|
Every request runs in a fresh `Scope` closed after the response is sent — `Effect.addFinalizer` in a handler runs post-response. Streaming bodies transfer the scope to the stream (`HttpEffect.scopeTransferToStream`, applied automatically by the adapters), so it closes when the body finishes streaming. If the client disconnects mid-request, the handler fiber is interrupted with the `ClientAbort` annotation (logged as `499`).
|
|
757
784
|
|
|
785
|
+
For failed handlers, request-scope finalizers receive the original failure even
|
|
786
|
+
when boundary handling successfully sends an error response.
|
|
787
|
+
|
|
758
788
|
### Integration tests on an ephemeral port
|
|
759
789
|
|
|
760
790
|
`NodeHttpServer.layerTest` starts a real server on port 0 and provides an `HttpClient` whose requests are rewritten to it:
|
|
@@ -762,7 +792,7 @@ Every request runs in a fresh `Scope` closed after the response is sent — `Eff
|
|
|
762
792
|
```ts
|
|
763
793
|
import { NodeHttpServer } from '@effect/platform-node';
|
|
764
794
|
import { describe, expect, it } from '@effect/vitest';
|
|
765
|
-
import { HttpClient, HttpClientResponse } from 'effect/
|
|
795
|
+
import { HttpClient, HttpClientResponse } from 'effect/http';
|
|
766
796
|
|
|
767
797
|
describe('todos', () => {
|
|
768
798
|
it.effect('GET /todos/:id', () =>
|
|
@@ -798,7 +828,7 @@ import {
|
|
|
798
828
|
HttpServerRespondable,
|
|
799
829
|
HttpServerResponse,
|
|
800
830
|
HttpStaticServer
|
|
801
|
-
} from 'effect/
|
|
831
|
+
} from 'effect/http';
|
|
802
832
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
803
833
|
import { createServer } from 'node:http';
|
|
804
834
|
|
|
@@ -930,7 +960,7 @@ describe('health', () => {
|
|
|
930
960
|
## Common Mistakes
|
|
931
961
|
|
|
932
962
|
1. **Using the v3 value-style router** — `HttpRouter.empty.pipe(HttpRouter.get('/x', app))` no longer exists. The v4 router is a service: register routes with `HttpRouter.add(...)` Layers (or `router.add` inside `HttpRouter.use`) and run with `HttpRouter.serve(appLayer)`.
|
|
933
|
-
2. **Importing from `@effect/platform`** — gone in v4. Everything is `effect/
|
|
963
|
+
2. **Importing from `@effect/platform`** — gone in v4. Everything is `effect/http`; only the platform adapters live in `@effect/platform-node` / `@effect/platform-bun`.
|
|
934
964
|
3. **Treating `HttpServerResponse.json` as synchronous** — it returns `Effect<HttpServerResponse, HttpBodyError>`. `yield*` it, or use `jsonUnsafe` for known-serializable values. Same for `schemaJson` (which is also curried: `schemaJson(schema)(value, options)`).
|
|
935
965
|
4. **Expecting `HttpServerResponse.empty()` to be 200** — the default status is `204`. Pass `{ status: 200 }` if you need it.
|
|
936
966
|
5. **Modifying the response in `HttpRouter.serve`'s `middleware` option** — that middleware wraps the chain *after* response sending; changes are silently dropped. Use `HttpRouter.middleware(..., { global: true })` or route-scoped `HttpRouter.middleware` instead.
|
|
@@ -85,7 +85,7 @@ export namespace MyModule {
|
|
|
85
85
|
return yield* db.findById(cfg.table, id);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
const list = Effect.
|
|
88
|
+
const list = Effect.gen(function* () {
|
|
89
89
|
const cfg = yield* config.get();
|
|
90
90
|
return yield* db.listAll(cfg.table);
|
|
91
91
|
});
|
|
@@ -127,6 +127,7 @@ export const defaultLayer = Layer.suspend(() =>
|
|
|
127
127
|
|
|
128
128
|
A shared `memoMap` ensures layers are deduplicated across all per-service runtimes. Define the bridge utility once and reuse it across migrated modules.
|
|
129
129
|
|
|
130
|
+
<!-- typecheck -->
|
|
130
131
|
```typescript
|
|
131
132
|
import { Layer, ManagedRuntime } from 'effect';
|
|
132
133
|
import type { Effect, Context } from 'effect';
|
|
@@ -137,13 +138,13 @@ export function makeRuntime<I, S, E>(
|
|
|
137
138
|
service: Context.Service<I, S>,
|
|
138
139
|
layer: Layer.Layer<I, E>
|
|
139
140
|
) {
|
|
140
|
-
|
|
141
|
-
const getRuntime = () => (rt ??= ManagedRuntime.make(layer, { memoMap }));
|
|
141
|
+
const runtime = ManagedRuntime.make(layer, { memoMap });
|
|
142
142
|
return {
|
|
143
143
|
runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) =>
|
|
144
|
-
|
|
144
|
+
runtime.runPromise(service.use(fn)),
|
|
145
145
|
runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) =>
|
|
146
|
-
|
|
146
|
+
runtime.runSync(service.use(fn)),
|
|
147
|
+
dispose: () => runtime.dispose()
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
```
|
|
@@ -151,9 +152,15 @@ export function makeRuntime<I, S, E>(
|
|
|
151
152
|
Then in the module namespace, create the bridge from the service and its default layer:
|
|
152
153
|
|
|
153
154
|
```typescript
|
|
154
|
-
const { runPromise } = makeRuntime(MyModule.Service, MyModule.defaultLayer);
|
|
155
|
+
const { runPromise, dispose } = makeRuntime(MyModule.Service, MyModule.defaultLayer);
|
|
155
156
|
```
|
|
156
157
|
|
|
158
|
+
`ManagedRuntime.make` builds the layer lazily on first use, so no separate lazy
|
|
159
|
+
runtime variable is needed. The host owns the bridge: stop admitting work and
|
|
160
|
+
await `dispose()` on shutdown. Disposal interrupts managed fibers and waits for
|
|
161
|
+
their cleanup before releasing layer resources. A shared memo map deduplicates
|
|
162
|
+
acquisition, but does not remove each runtime's disposal obligation.
|
|
163
|
+
|
|
157
164
|
> **Memo-map nuance:** Keep the bridge `memoMap` shared (the root `Layer.makeMemoMapUnsafe()` above) so every per-service runtime reuses the same layer allocations. Do not `Layer.forkMemoMap` it unless a specific child runtime intentionally needs isolated allocations — a forked memo map can read the parent's existing allocations but builds new ones in isolation, which defeats the deduplication this bridge exists to provide.
|
|
158
165
|
|
|
159
166
|
### Step 6: Keep Boundary Facades Only When Still Needed
|
|
@@ -297,7 +304,7 @@ export namespace Items {
|
|
|
297
304
|
});
|
|
298
305
|
});
|
|
299
306
|
|
|
300
|
-
const list = Effect.
|
|
307
|
+
const list = Effect.gen(function* () {
|
|
301
308
|
const cfg = yield* config.load();
|
|
302
309
|
const res = yield* Effect.tryPromise({
|
|
303
310
|
try: () => fetch(`${cfg.apiUrl}/items`),
|
|
@@ -321,7 +328,9 @@ export namespace Items {
|
|
|
321
328
|
);
|
|
322
329
|
|
|
323
330
|
// Step 5: Runtime bridge
|
|
324
|
-
const
|
|
331
|
+
const runtime = makeRuntime(Service, defaultLayer);
|
|
332
|
+
const { runPromise } = runtime;
|
|
333
|
+
export const dispose = runtime.dispose; // host awaits this at shutdown
|
|
325
334
|
|
|
326
335
|
// Step 6: Async facades (remove once all callers migrate)
|
|
327
336
|
export async function get(id: string): Promise<Item> {
|
|
@@ -467,6 +467,14 @@ const program = Effect.all([
|
|
|
467
467
|
error type. Preloading a LayerMap does not make future resource acquisition
|
|
468
468
|
infallible; its accessors retain the resource error channel.
|
|
469
469
|
|
|
470
|
+
Construct keyed layers with a lookup function and options:
|
|
471
|
+
`LayerMap.make(lookup, { preloadKeys, idleTimeToLive })`.
|
|
472
|
+
`fromRecord` and `LayerMap.Service` also support preloading. In all forms, keys
|
|
473
|
+
with zero idle TTL are skipped, including when TTL is omitted. Set a non-zero
|
|
474
|
+
`idleTimeToLive` to eagerly acquire and validate preloaded keys during
|
|
475
|
+
construction; otherwise errors surface on first access. Preloaded resources
|
|
476
|
+
remain retained only for that idle TTL, not forever.
|
|
477
|
+
|
|
470
478
|
Handle construction errors:
|
|
471
479
|
|
|
472
480
|
```typescript
|
|
@@ -124,6 +124,12 @@ Return `Exit<A, E | ER>` instead of throwing — useful when you want to inspect
|
|
|
124
124
|
|
|
125
125
|
ManagedRuntime **owns the scope** of the layers it builds. When you dispose the runtime, all resources acquired during layer construction (database pools, HTTP clients, file handles, etc.) are released.
|
|
126
126
|
|
|
127
|
+
Disposal first interrupts managed fibers and **waits for their finalizers**, then
|
|
128
|
+
releases layer resources. Request cleanup can therefore still use its database
|
|
129
|
+
or other layer services. Layer finalization runs uninterruptibly by default;
|
|
130
|
+
await `dispose()` / `disposeEffect` before treating shutdown as complete. This
|
|
131
|
+
ordering does not supervise work deliberately detached from the runtime.
|
|
132
|
+
|
|
127
133
|
### Disposing the runtime
|
|
128
134
|
|
|
129
135
|
```ts
|