opencode-effect-enforcer 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +15 -305
- package/guidance/progressive-disclosure-guidance.md +18 -24
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +2 -2
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +4 -4
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
- package/src/guidance.ts +0 -1
|
@@ -3,39 +3,39 @@ name: effect-rpc-cluster
|
|
|
3
3
|
description: Build typed RPC endpoints and cluster-distributed entities, singletons, cron jobs, and durable workflows with Effect's RPC and Cluster modules (Rpc/RpcGroup/RpcServer/RpcClient, Entity/Sharding/Singleton, Node/Bun bundles). Use when building RPC services or distributed/clustered Effect systems.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are an Effect TypeScript expert specializing in `effect/
|
|
6
|
+
You are an Effect TypeScript expert specializing in `effect/rpc` and `effect/cluster`.
|
|
7
7
|
|
|
8
|
-
These modules live under `effect
|
|
8
|
+
These modules live under `effect/*`. There are no `@effect/rpc` or `@effect/cluster` packages in v4 — everything ships from the `effect` package. They remain `@stability unstable` and may break in minor releases. Keep Effect-family packages on the same release.
|
|
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/`.
|
|
12
|
+
The Effect v4 source is 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.
|
|
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/
|
|
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/
|
|
36
|
-
- `packages/effect/src/
|
|
37
|
-
- `packages/effect/src/
|
|
38
|
-
- `packages/effect/src/
|
|
16
|
+
- `packages/effect/src/rpc/Rpc.ts` — `Rpc.make`, custom constructors, `Wrapper`, `ServerClient`, `exitSchema`
|
|
17
|
+
- `packages/effect/src/rpc/RpcGroup.ts` — group construction, handler wiring (`toLayer` / `toHandlers` / `toLayerHandler` / `accessHandler`), prefixing, omit/merge, annotations
|
|
18
|
+
- `packages/effect/src/rpc/RpcServer.ts` — `make`, `layer`, `layerHttp`, every `layerProtocol*` and `toHttpEffect*`
|
|
19
|
+
- `packages/effect/src/rpc/RpcClient.ts` — `make`, `Protocol`, every `layerProtocol*`, `withHeaders`, `CurrentHeaders`, `ConnectionHooks`
|
|
20
|
+
- `packages/effect/src/rpc/RpcMiddleware.ts` — `Service` constructor, `layerClient`
|
|
21
|
+
- `packages/effect/src/rpc/RpcSerialization.ts` — JSON/NDJSON/JSON-RPC/SchemaBinary codecs and layers
|
|
22
|
+
- `packages/effect/src/rpc/RpcTest.ts` — in-process test client
|
|
23
|
+
- `packages/effect/src/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
|
|
24
|
+
- `packages/effect/src/rpc/RpcSchema.ts` — `Stream` schema marker, `ClientAbort` cause annotation
|
|
25
|
+
- `packages/effect/src/rpc/RpcClientError.ts` — client-side error union
|
|
26
|
+
- `packages/effect/src/cluster/Entity.ts` — `Entity.make` / `fromRpcGroup`, handler envelopes, `Replier`, `CurrentAddress`, `keepAlive`, `makeTestClient`
|
|
27
|
+
- `packages/effect/src/cluster/ClusterSchema.ts` — `Persisted`, `Uninterruptible`, `WithTransaction`, `ShardGroup`, `ClientTracingEnabled`, `Dynamic`
|
|
28
|
+
- `packages/effect/src/cluster/ClusterError.ts` — `MailboxFull`, `AlreadyProcessingMessage`, `PersistenceError`, `EntityNotAssignedToRunner`, `MalformedMessage`, `RunnerUnavailable`, `RunnerNotRegistered`
|
|
29
|
+
- `packages/effect/src/cluster/Sharding.ts` — the `Sharding` service surface
|
|
30
|
+
- `packages/effect/src/cluster/ShardingConfig.ts` — config schema + env loader
|
|
31
|
+
- `packages/effect/src/cluster/Singleton.ts` — singleton-per-cluster effects
|
|
32
|
+
- `packages/effect/src/cluster/ClusterCron.ts` — cron-driven singletons
|
|
33
|
+
- `packages/effect/src/cluster/SingleRunner.ts` — single-node sql-backed bundle
|
|
34
|
+
- `packages/effect/src/cluster/TestRunner.ts` — in-memory testing bundle
|
|
35
|
+
- `packages/effect/src/cluster/EntityProxy.ts` + `EntityProxyServer.ts` — entity ↔ RPC/HTTP bridge
|
|
36
|
+
- `packages/effect/src/workflow/WorkflowProxy.ts` + `WorkflowProxyServer.ts` — workflow ↔ RPC/HTTP bridge
|
|
37
|
+
- `packages/effect/src/cluster/ClusterWorkflowEngine.ts` — production workflow engine backed by sharding + storage
|
|
38
|
+
- `packages/effect/src/reactivity/AtomRpc.ts` — reactive RPC client for Atom UIs (see also `effect-atom-rpc` skill)
|
|
39
39
|
- `packages/platform/node/src/NodeClusterHttp.ts` / `NodeClusterSocket.ts` — Node "all-in-one" cluster layers
|
|
40
40
|
- `packages/platform/bun/src/BunClusterHttp.ts` / `BunClusterSocket.ts` — Bun equivalents
|
|
41
41
|
- `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — best end-to-end reference for real RPC wiring
|
|
@@ -55,8 +55,8 @@ import {
|
|
|
55
55
|
RpcServer,
|
|
56
56
|
RpcTest,
|
|
57
57
|
RpcWorker
|
|
58
|
-
} from 'effect/
|
|
59
|
-
import { RpcClientError } from 'effect/
|
|
58
|
+
} from 'effect/rpc';
|
|
59
|
+
import { RpcClientError } from 'effect/rpc/RpcClientError';
|
|
60
60
|
|
|
61
61
|
// Cluster
|
|
62
62
|
import {
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
SqlMessageStorage,
|
|
78
78
|
SqlRunnerStorage,
|
|
79
79
|
TestRunner
|
|
80
|
-
} from 'effect/
|
|
80
|
+
} from 'effect/cluster';
|
|
81
81
|
|
|
82
82
|
// Workflow (see effect-workflow skill for the full surface)
|
|
83
83
|
import {
|
|
@@ -87,8 +87,8 @@ import {
|
|
|
87
87
|
Workflow,
|
|
88
88
|
WorkflowProxy,
|
|
89
89
|
WorkflowProxyServer
|
|
90
|
-
} from 'effect/
|
|
91
|
-
import { ClusterWorkflowEngine } from 'effect/
|
|
90
|
+
} from 'effect/workflow';
|
|
91
|
+
import { ClusterWorkflowEngine } from 'effect/cluster';
|
|
92
92
|
|
|
93
93
|
// Platform "all-in-one" cluster bundles
|
|
94
94
|
import { NodeClusterHttp, NodeClusterSocket } from '@effect/platform-node';
|
|
@@ -133,7 +133,7 @@ Two big invariants:
|
|
|
133
133
|
|
|
134
134
|
```ts
|
|
135
135
|
import { Schema } from 'effect';
|
|
136
|
-
import { Rpc } from 'effect/
|
|
136
|
+
import { Rpc } from 'effect/rpc';
|
|
137
137
|
|
|
138
138
|
// Style A — const value. Compact, fine for ad-hoc rpcs.
|
|
139
139
|
export const Ping = Rpc.make('Ping', { success: Schema.String });
|
|
@@ -151,7 +151,9 @@ Both styles are official. The platform-node test fixtures and the cluster test f
|
|
|
151
151
|
- `class extends` when the rpc is shared across many modules and the nominal type helps documentation/imports
|
|
152
152
|
- `const` when you're listing a dozen rpcs in one file and the boilerplate hurts more than the nominal type helps
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
`RpcGroup.make` and `Workflow.make` also support class-extends declarations;
|
|
155
|
+
`Activity.make` and `Entity.make` use plain values. Prefer the convention already
|
|
156
|
+
used by the shared contract package.
|
|
155
157
|
|
|
156
158
|
### `Rpc.make` options
|
|
157
159
|
|
|
@@ -273,7 +275,7 @@ Returns a `Schema.Exit<Success, Error, Defect>` for the rpc that includes any mi
|
|
|
273
275
|
Rare but powerful: build a constructor that transforms every rpc's success/error schemas. Lets you encode a convention like "every list endpoint returns a paginated wrapper":
|
|
274
276
|
|
|
275
277
|
```ts
|
|
276
|
-
import { Rpc } from 'effect/
|
|
278
|
+
import { Rpc } from 'effect/rpc';
|
|
277
279
|
import { Schema } from 'effect';
|
|
278
280
|
|
|
279
281
|
interface PaginatedRpc extends Rpc.Custom {
|
|
@@ -434,8 +436,8 @@ Returns an `Effect<Context.Context<Rpc.ToHandler<R>>>` — the unprovided form o
|
|
|
434
436
|
Returns an Effect that resolves to a single handler function with `services` already provided. The handler is callable as `(payload, options)` directly. This is the easiest way to unit-test one rpc handler in isolation:
|
|
435
437
|
|
|
436
438
|
```ts
|
|
437
|
-
import { Headers } from 'effect/
|
|
438
|
-
import { RequestId } from 'effect/
|
|
439
|
+
import { Headers } from 'effect/http';
|
|
440
|
+
import { RequestId } from 'effect/rpc/RpcMessage';
|
|
439
441
|
|
|
440
442
|
const result =
|
|
441
443
|
yield*
|
|
@@ -462,7 +464,7 @@ Requires a `Protocol` in context (one of the `RpcServer.layerProtocol*`), the ha
|
|
|
462
464
|
|
|
463
465
|
```ts
|
|
464
466
|
import { Layer } from 'effect';
|
|
465
|
-
import { HttpRouter } from 'effect/
|
|
467
|
+
import { HttpRouter } from 'effect/http';
|
|
466
468
|
|
|
467
469
|
const ServerLayer = RpcServer.layer(UsersGroup, {
|
|
468
470
|
concurrency: 'unbounded', // default; set a number to backpressure handlers
|
|
@@ -473,11 +475,17 @@ const ServerLayer = RpcServer.layer(UsersGroup, {
|
|
|
473
475
|
}).pipe(
|
|
474
476
|
Layer.provide(UsersLive), // handlers
|
|
475
477
|
Layer.provide(RpcServer.layerProtocolHttp({ path: '/rpc' })),
|
|
476
|
-
Layer.provide(RpcSerialization.layerNdjson)
|
|
477
|
-
Layer.provide(HttpRouter.layer)
|
|
478
|
+
Layer.provide(RpcSerialization.layerNdjson)
|
|
478
479
|
);
|
|
479
480
|
```
|
|
480
481
|
|
|
482
|
+
Pass `ServerLayer` to `HttpRouter.serve`, `toWebHandler`, or `toHttpEffect` with
|
|
483
|
+
its router requirement intact. `serve` and `toHttpEffect` build a fresh router in
|
|
484
|
+
a forked memo map; `toWebHandler` builds separately by default but passes an
|
|
485
|
+
explicit `memoMap` through unchanged. Pre-providing `HttpRouter.layer` would register on a different router.
|
|
486
|
+
Layers first built inside the entrypoint are private. Provide stateful services
|
|
487
|
+
shared by sibling servers outside those entrypoints.
|
|
488
|
+
|
|
481
489
|
Server options:
|
|
482
490
|
|
|
483
491
|
- **`concurrency: number | 'unbounded'`** (default `'unbounded'`) — one semaphore around handler execution for the whole server instance, shared by all clients. `Rpc.fork(...)` opts a single handler out of this limit.
|
|
@@ -498,8 +506,7 @@ const ServerLayer = RpcServer.layerHttp({
|
|
|
498
506
|
streamBufferSize: 16 // framed HTTP response queue; default 16
|
|
499
507
|
}).pipe(
|
|
500
508
|
Layer.provide(UsersLive),
|
|
501
|
-
Layer.provide(RpcSerialization.layerNdjson)
|
|
502
|
-
Layer.provide(HttpRouter.layer)
|
|
509
|
+
Layer.provide(RpcSerialization.layerNdjson)
|
|
503
510
|
);
|
|
504
511
|
```
|
|
505
512
|
|
|
@@ -618,7 +625,7 @@ messages additionally have their documented storage/delivery outcomes.
|
|
|
618
625
|
For one-off headers, use the per-call `headers` option. For region-scoped headers, use `RpcClient.withHeaders` (which updates the `RpcClient.CurrentHeaders` Reference):
|
|
619
626
|
|
|
620
627
|
```ts
|
|
621
|
-
import { RpcClient } from 'effect/
|
|
628
|
+
import { RpcClient } from 'effect/rpc';
|
|
622
629
|
|
|
623
630
|
yield* program.pipe(
|
|
624
631
|
RpcClient.withHeaders({ authorization: `Bearer ${token}`, userid: '123' })
|
|
@@ -673,6 +680,11 @@ Pattern-match on `error.reason._tag` to handle transport faults (network down, m
|
|
|
673
680
|
| `RpcClient.layerProtocolSocket({ retryTransientErrors?, onTransientError? })` | `RpcSerialization`, `Socket.Socket` | full duplex. Auto-pings every 5s; reconnects on transient socket errors; reports retried open failures through `onTransientError` |
|
|
674
681
|
| `RpcClient.layerProtocolWorker(options)` | `Worker.WorkerPlatform`, `Worker.Spawner` | pool of worker-backed clients. Options: either `{ size, concurrency?, targetUtilization? }` or `{ minSize, maxSize, timeToLive, concurrency?, targetUtilization? }` |
|
|
675
682
|
|
|
683
|
+
A missed pong is a `SocketReadError`, fails all in-flight calls, and triggers
|
|
684
|
+
reconnection even with `retryTransientErrors: true`. It does not invoke
|
|
685
|
+
`onTransientError`; that hook applies to retried connection-open failures.
|
|
686
|
+
Reconnection does not replay failed calls. Retry only idempotent calls explicitly.
|
|
687
|
+
|
|
676
688
|
For each there's a corresponding `make*` Effect (`makeProtocolHttp`, `makeProtocolSocket`, `makeProtocolWorker`) when you need finer control over context.
|
|
677
689
|
|
|
678
690
|
### `RpcClient.ConnectionHooks`
|
|
@@ -694,7 +706,7 @@ not a Cause; use `onExit` to distinguish client cancel from server shutdown:
|
|
|
694
706
|
|
|
695
707
|
<!-- typecheck -->
|
|
696
708
|
```ts
|
|
697
|
-
import { RpcSchema } from 'effect/
|
|
709
|
+
import { RpcSchema } from 'effect/rpc';
|
|
698
710
|
import { Cause, Effect, Exit, Stream } from 'effect';
|
|
699
711
|
import * as Arr from 'effect/Array';
|
|
700
712
|
|
|
@@ -713,7 +725,7 @@ const subscribeHandler = stream.pipe(Stream.runDrain,
|
|
|
713
725
|
`RpcMiddleware.Service<Self, Config>()(name, options)` defines a middleware service. The config positionally encodes what the middleware *provides*, *requires*, and what *client-only* error type it can throw. The options carry the wire-error schema and the `requiredForClient` enforcement flag.
|
|
714
726
|
|
|
715
727
|
```ts
|
|
716
|
-
import { RpcMiddleware } from 'effect/
|
|
728
|
+
import { RpcMiddleware } from 'effect/rpc';
|
|
717
729
|
import { Context, Schema } from 'effect';
|
|
718
730
|
|
|
719
731
|
class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
|
|
@@ -766,7 +778,7 @@ Middleware can chain `requires` and `provides` — `DbMiddleware extends RpcMidd
|
|
|
766
778
|
For middleware that needs to *also* run client-side (most commonly: attach an auth header), provide a `layerClient`:
|
|
767
779
|
|
|
768
780
|
```ts
|
|
769
|
-
import { Headers } from 'effect/
|
|
781
|
+
import { Headers } from 'effect/http';
|
|
770
782
|
|
|
771
783
|
export const AuthClient = RpcMiddleware.layerClient(
|
|
772
784
|
AuthMiddleware,
|
|
@@ -780,7 +792,7 @@ export const AuthClient = RpcMiddleware.layerClient(
|
|
|
780
792
|
|
|
781
793
|
Important details:
|
|
782
794
|
|
|
783
|
-
- `request.headers` is `Headers.Headers` (already parsed). Use the helpers from `effect/
|
|
795
|
+
- `request.headers` is `Headers.Headers` (already parsed). Use the helpers from `effect/http/Headers` (`Headers.set`, `Headers.merge`, `Headers.fromInput`).
|
|
784
796
|
- You **must** call `next(request)` (with the modified or original request) — the middleware's job is to wrap the send, not replace it.
|
|
785
797
|
- The Layer signature is `Layer<ForClient<AuthMiddleware>>` — it's a distinct service from the server-side middleware, and providing both is the norm for client packages.
|
|
786
798
|
|
|
@@ -810,7 +822,7 @@ same defect fallback that storage records.
|
|
|
810
822
|
|
|
811
823
|
Picking the wrong one is a real bug:
|
|
812
824
|
|
|
813
|
-
- `layerJson` over
|
|
825
|
+
- `layerJson` over raw TCP → no framing → message boundaries cannot be recovered; WebSocket frames whole messages and supports plain JSON
|
|
814
826
|
- SchemaBinary against a JSON-only HTTP client → incompatible responses
|
|
815
827
|
- `layerNdjson` against `layerProtocolHttp` → streams incrementally through a bounded response queue (default 16), without RPC acks
|
|
816
828
|
|
|
@@ -820,7 +832,7 @@ In-process server+client wired together, no network. The simplest possible RPC t
|
|
|
820
832
|
|
|
821
833
|
```ts
|
|
822
834
|
import { Effect, Layer } from 'effect';
|
|
823
|
-
import { RpcTest } from 'effect/
|
|
835
|
+
import { RpcTest } from 'effect/rpc';
|
|
824
836
|
import { it } from '@effect/vitest';
|
|
825
837
|
|
|
826
838
|
const TestClient = Layer.effect(UsersClient)(
|
|
@@ -843,7 +855,7 @@ For worker-backed clients, you can pass typed initial config at spawn time witho
|
|
|
843
855
|
|
|
844
856
|
```ts
|
|
845
857
|
// On the worker (server side):
|
|
846
|
-
import { RpcWorker } from 'effect/
|
|
858
|
+
import { RpcWorker } from 'effect/rpc';
|
|
847
859
|
|
|
848
860
|
class WorkerConfig extends Schema.Class<WorkerConfig>('WorkerConfig')({
|
|
849
861
|
apiUrl: Schema.String,
|
|
@@ -875,8 +887,8 @@ Cluster turns rpcs into **addressable, distributed actors** (`Entity`). Messages
|
|
|
875
887
|
|
|
876
888
|
```ts
|
|
877
889
|
import { Schema } from 'effect';
|
|
878
|
-
import { ClusterSchema, Entity } from 'effect/
|
|
879
|
-
import { Rpc } from 'effect/
|
|
890
|
+
import { ClusterSchema, Entity } from 'effect/cluster';
|
|
891
|
+
import { Rpc } from 'effect/rpc';
|
|
880
892
|
|
|
881
893
|
export class Increment extends Rpc.make('Increment', {
|
|
882
894
|
payload: { amount: Schema.Number },
|
|
@@ -958,7 +970,7 @@ export const CounterLive = Counter.toLayer(
|
|
|
958
970
|
Entity handlers can pull two services from context:
|
|
959
971
|
|
|
960
972
|
```ts
|
|
961
|
-
import { Entity } from 'effect/
|
|
973
|
+
import { Entity } from 'effect/cluster';
|
|
962
974
|
|
|
963
975
|
Counter.toLayer(Effect.gen(function*() {
|
|
964
976
|
return Counter.of({
|
|
@@ -1108,7 +1120,7 @@ In-process entity testing without a real cluster. Returns `(entityId) => Effect<
|
|
|
1108
1120
|
|
|
1109
1121
|
```ts
|
|
1110
1122
|
import { Effect } from 'effect';
|
|
1111
|
-
import { Entity, ShardingConfig } from 'effect/
|
|
1123
|
+
import { Entity, ShardingConfig } from 'effect/cluster';
|
|
1112
1124
|
import { it } from '@effect/vitest';
|
|
1113
1125
|
|
|
1114
1126
|
const TestShardingConfig = ShardingConfig.layer({
|
|
@@ -1132,7 +1144,7 @@ Required context: `Scope | ShardingConfig | Rpc.MiddlewareClient<Rpcs> | (handle
|
|
|
1132
1144
|
A `Singleton` is an effect that runs *exactly once across the cluster*. The shard manager elects a single runner to host it; if that runner dies, another one takes over. Distinct from `ClusterCron` — the latter is built on top of singletons + entities.
|
|
1133
1145
|
|
|
1134
1146
|
```ts
|
|
1135
|
-
import { Singleton } from 'effect/
|
|
1147
|
+
import { Singleton } from 'effect/cluster';
|
|
1136
1148
|
|
|
1137
1149
|
const LeaderElection = Singleton.make(
|
|
1138
1150
|
'leader-elector',
|
|
@@ -1154,7 +1166,7 @@ Cluster-singleton cron executions. The cron schedule is durable: missed runs (wi
|
|
|
1154
1166
|
|
|
1155
1167
|
```ts
|
|
1156
1168
|
import { Cron, Effect } from 'effect';
|
|
1157
|
-
import { ClusterCron } from 'effect/
|
|
1169
|
+
import { ClusterCron } from 'effect/cluster';
|
|
1158
1170
|
|
|
1159
1171
|
const DailyReport = ClusterCron.make({
|
|
1160
1172
|
name: 'DailyReport',
|
|
@@ -1178,7 +1190,7 @@ The schedule survives runner restarts because the next invocation is durably sto
|
|
|
1178
1190
|
Inside any entity handler (or any effect running in the cluster), you can pull `Sharding.Sharding` for cluster-aware operations:
|
|
1179
1191
|
|
|
1180
1192
|
```ts
|
|
1181
|
-
import { Sharding } from 'effect/
|
|
1193
|
+
import { Sharding } from 'effect/cluster';
|
|
1182
1194
|
|
|
1183
1195
|
const sharding = yield* Sharding.Sharding;
|
|
1184
1196
|
|
|
@@ -1197,7 +1209,7 @@ Three levels of convenience.
|
|
|
1197
1209
|
### Level 1: testing — `TestRunner.layer`
|
|
1198
1210
|
|
|
1199
1211
|
```ts
|
|
1200
|
-
import { TestRunner } from 'effect/
|
|
1212
|
+
import { TestRunner } from 'effect/cluster';
|
|
1201
1213
|
|
|
1202
1214
|
const TestLayer = Layer.mergeAll(CounterLive, OrderLive).pipe(
|
|
1203
1215
|
Layer.provideMerge(TestRunner.layer)
|
|
@@ -1217,7 +1229,7 @@ expect(driver.journal[0].address.entityId).toBe('test-1');
|
|
|
1217
1229
|
### Level 2: single-node, sql-backed — `SingleRunner.layer`
|
|
1218
1230
|
|
|
1219
1231
|
```ts
|
|
1220
|
-
import { SingleRunner } from 'effect/
|
|
1232
|
+
import { SingleRunner } from 'effect/cluster';
|
|
1221
1233
|
|
|
1222
1234
|
const ClusterLayer = SingleRunner.layer({
|
|
1223
1235
|
shardingConfig: { entityMaxIdleTime: '10 minutes' },
|
|
@@ -1296,7 +1308,7 @@ When the bundles aren't quite right, assemble from the primitives:
|
|
|
1296
1308
|
`availableShardGroups` is **cluster-wide** and must be identical on every runner that shares the same storage backend — shard and advisory-lock numbering is derived from it. `assignedShardGroups` is per-runner and is filtered against `availableShardGroups`, so a runner only ever owns groups that appear in *both*. If your code routes entities or workflows to a non-`default` `ClusterSchema.ShardGroup`, that group must be in `availableShardGroups` everywhere and in `assignedShardGroups` on the runners meant to host it:
|
|
1297
1309
|
|
|
1298
1310
|
```ts
|
|
1299
|
-
import { ShardingConfig } from 'effect/
|
|
1311
|
+
import { ShardingConfig } from 'effect/cluster';
|
|
1300
1312
|
|
|
1301
1313
|
const Config = ShardingConfig.layer({
|
|
1302
1314
|
availableShardGroups: ['default', 'workflow'], // cluster-wide; same on all runners
|
|
@@ -1321,6 +1333,32 @@ The storage poller reads at most `unprocessedMessageBatchSize` messages per batc
|
|
|
1321
1333
|
|
|
1322
1334
|
The memory driver now uses the same ten-minute claim window as SQL. `resetAddress`/`resetAddresses` or `resetShards` makes claimed messages immediately eligible again, which prevents bounded reads from repeatedly selecting in-flight rows while still allowing explicit recovery.
|
|
1323
1335
|
|
|
1336
|
+
Both decoded and encoded storage services also require `resetRequests(ids)`:
|
|
1337
|
+
release those request claims without changing replies or processed state. This
|
|
1338
|
+
lets a reset workflow request be redelivered promptly when its prior completion
|
|
1339
|
+
is still deduplicated locally. Memory storage models claim expiry but not SQL
|
|
1340
|
+
reply filtering or transaction isolation; use SQL-backed tests for those rules.
|
|
1341
|
+
|
|
1342
|
+
`clearReplies(requestId, { expectedReplyId? })` must compare the **latest** reply
|
|
1343
|
+
ID atomically at the storage boundary before clearing when an expected ID is
|
|
1344
|
+
provided. A mismatch is a successful no-op; without the option clearing is
|
|
1345
|
+
unconditional. `Sharding.reset(requestId, options?)` forwards that condition and
|
|
1346
|
+
returns `false` only when clearing fails, not on a comparison mismatch. Custom
|
|
1347
|
+
stores that ignore the condition allow stale workflow resumes to erase newer
|
|
1348
|
+
completed replies. Cluster workflows retry failed resets before acknowledging
|
|
1349
|
+
deferred completion.
|
|
1350
|
+
|
|
1351
|
+
With message storage disabled, completed request IDs are not retained for
|
|
1352
|
+
deduplication. Redelivery runs the request again; design volatile handlers for
|
|
1353
|
+
that possibility rather than expecting `AlreadyProcessingMessage` forever.
|
|
1354
|
+
|
|
1355
|
+
Local sends waiting on a missing entity type share the runner's
|
|
1356
|
+
`entityRegistrationTimeout` deadline with storage reads. They defect with
|
|
1357
|
+
`Entity type ... not registered` when it expires. The window is not renewed per
|
|
1358
|
+
send; dynamically registered types first contacted after that deadline fail
|
|
1359
|
+
immediately until registered. If registration never starts, the fallback window
|
|
1360
|
+
is two timeout intervals from the first missing type observed.
|
|
1361
|
+
|
|
1324
1362
|
For custom SQL composition, `SqlMessageStorage.makeEncoded({ prefix? })` returns the low-level `MessageStorage.Encoded` driver directly. `SqlMessageStorage.make`, `layer`, and `layerWith` remain the decoded service constructors.
|
|
1325
1363
|
|
|
1326
1364
|
## Bridges — exposing entities and workflows as RPC/HTTP
|
|
@@ -1330,9 +1368,9 @@ Both `Entity` and `Workflow` ship "proxy" helpers that auto-derive `RpcGroup`s a
|
|
|
1330
1368
|
### `EntityProxy` — entity → RPC / HTTP
|
|
1331
1369
|
|
|
1332
1370
|
```ts
|
|
1333
|
-
import { Entity, EntityProxy, EntityProxyServer } from 'effect/
|
|
1334
|
-
import { RpcServer } from 'effect/
|
|
1335
|
-
import { HttpApi, HttpApiBuilder } from 'effect/
|
|
1371
|
+
import { Entity, EntityProxy, EntityProxyServer } from 'effect/cluster';
|
|
1372
|
+
import { RpcServer } from 'effect/rpc';
|
|
1373
|
+
import { HttpApi, HttpApiBuilder } from 'effect/http-api';
|
|
1336
1374
|
|
|
1337
1375
|
const Counter = Entity.make('Counter', [Increment, GetCount])
|
|
1338
1376
|
.annotateRpcs(ClusterSchema.Persisted, true);
|
|
@@ -1378,8 +1416,8 @@ remotes rather than adding an independent retry loop.
|
|
|
1378
1416
|
### `WorkflowProxy` — workflow → RPC / HTTP
|
|
1379
1417
|
|
|
1380
1418
|
```ts
|
|
1381
|
-
import { Workflow, WorkflowProxy, WorkflowProxyServer } from 'effect/
|
|
1382
|
-
import { RpcServer } from 'effect/
|
|
1419
|
+
import { Workflow, WorkflowProxy, WorkflowProxyServer } from 'effect/workflow';
|
|
1420
|
+
import { RpcServer } from 'effect/rpc';
|
|
1383
1421
|
|
|
1384
1422
|
const myWorkflows = [EmailWorkflow, OrderWorkflow] as const;
|
|
1385
1423
|
|
|
@@ -1410,8 +1448,8 @@ discards even that ID. Entity discard endpoints keep their separate contract.
|
|
|
1410
1448
|
The in-memory `WorkflowEngine.layerMemory` is for testing only. For production, use `ClusterWorkflowEngine.layer`, which wires the workflow engine into the cluster's `Sharding` + `MessageStorage`:
|
|
1411
1449
|
|
|
1412
1450
|
```ts
|
|
1413
|
-
import { ClusterWorkflowEngine } from 'effect/
|
|
1414
|
-
import { Workflow } from 'effect/
|
|
1451
|
+
import { ClusterWorkflowEngine } from 'effect/cluster';
|
|
1452
|
+
import { Workflow } from 'effect/workflow';
|
|
1415
1453
|
|
|
1416
1454
|
const WorkflowsLayer = Layer.mergeAll(
|
|
1417
1455
|
EmailWorkflowLayer,
|
|
@@ -1426,9 +1464,9 @@ const WorkflowsLayer = Layer.mergeAll(
|
|
|
1426
1464
|
A workflow can be annotated with `ClusterSchema.ShardGroup`, exactly like an entity:
|
|
1427
1465
|
|
|
1428
1466
|
```ts
|
|
1429
|
-
import { ClusterSchema } from 'effect/
|
|
1467
|
+
import { ClusterSchema } from 'effect/cluster';
|
|
1430
1468
|
|
|
1431
|
-
const OrderWorkflow = Workflow.make({ /* ... */ })
|
|
1469
|
+
const OrderWorkflow = Workflow.make('OrderWorkflow', { /* ... */ })
|
|
1432
1470
|
.annotate(ClusterSchema.ShardGroup, () => 'workflow');
|
|
1433
1471
|
```
|
|
1434
1472
|
|
|
@@ -1436,6 +1474,12 @@ const OrderWorkflow = Workflow.make({ /* ... */ })
|
|
|
1436
1474
|
|
|
1437
1475
|
Workflow execution entities and the durable-clock entity use a fixed `10 seconds` idle timeout. Completed and suspended workflows therefore release runner residency slots quickly; their durable state is reconstructed from storage when the next resume, deferred completion, or clock message arrives. Do not use `Entity.keepAlive` to pin these internal workflow entities.
|
|
1438
1476
|
|
|
1477
|
+
Execution IDs now hash `"${name.length}:${name}:${idempotencyKey(payload)}"`.
|
|
1478
|
+
Recomputing IDs created under rc.116 yields new identities; retain stored IDs
|
|
1479
|
+
when operating on existing runs. Reusing a workflow tag with a different
|
|
1480
|
+
definition warns and retains the existing definition. See `effect-workflow` for
|
|
1481
|
+
the execution-identity and deferred-token boundaries.
|
|
1482
|
+
|
|
1439
1483
|
See the `effect-workflow` skill for the full `Workflow` / `Activity` / `DurableClock` / `DurableDeferred` / `DurableQueue` API surface.
|
|
1440
1484
|
|
|
1441
1485
|
## Reactive frontend — `AtomRpc`
|
|
@@ -1443,7 +1487,7 @@ See the `effect-workflow` skill for the full `Workflow` / `Activity` / `DurableC
|
|
|
1443
1487
|
`AtomRpc.Service()(...)` produces an Atom-aware RPC client with `query` (cached, reactive) and `mutation` (invalidating) helpers, designed for React + Atom apps. See the `effect-atom-rpc` skill for the full surface; brief teaser:
|
|
1444
1488
|
|
|
1445
1489
|
```ts
|
|
1446
|
-
import { AtomRpc } from 'effect/
|
|
1490
|
+
import { AtomRpc } from 'effect/reactivity';
|
|
1447
1491
|
|
|
1448
1492
|
class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
|
|
1449
1493
|
group: UsersGroup,
|
|
@@ -1473,7 +1517,7 @@ A small users service with auth middleware, websocket transport, and a test clie
|
|
|
1473
1517
|
```ts
|
|
1474
1518
|
// --- definitions/users.ts (shared between server and client) ---
|
|
1475
1519
|
import { Context, Schema } from 'effect';
|
|
1476
|
-
import { Rpc, RpcGroup, RpcMiddleware } from 'effect/
|
|
1520
|
+
import { Rpc, RpcGroup, RpcMiddleware } from 'effect/rpc';
|
|
1477
1521
|
|
|
1478
1522
|
export class User extends Schema.Class<User>('User')({
|
|
1479
1523
|
id: Schema.String,
|
|
@@ -1516,8 +1560,8 @@ export const UsersGroup = RpcGroup.make(GetUser, StreamUsers).middleware(AuthMid
|
|
|
1516
1560
|
```ts
|
|
1517
1561
|
// --- server/handlers.ts ---
|
|
1518
1562
|
import { Effect, Layer, Stream } from 'effect';
|
|
1519
|
-
import { Headers } from 'effect/
|
|
1520
|
-
import { Rpc, RpcMiddleware } from 'effect/
|
|
1563
|
+
import { Headers } from 'effect/http';
|
|
1564
|
+
import { Rpc, RpcMiddleware } from 'effect/rpc';
|
|
1521
1565
|
import { CurrentUser, UnauthorizedError, UsersGroup, User, UserNotFound } from '../definitions/users.ts';
|
|
1522
1566
|
|
|
1523
1567
|
export const UsersHandlersLive = UsersGroup.toLayer(
|
|
@@ -1549,8 +1593,8 @@ export const AuthLive = Layer.succeed(AuthMiddleware)(
|
|
|
1549
1593
|
// --- server/main.ts ---
|
|
1550
1594
|
import { Layer } from 'effect';
|
|
1551
1595
|
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
|
|
1552
|
-
import { HttpRouter } from 'effect/
|
|
1553
|
-
import { RpcSerialization, RpcServer } from 'effect/
|
|
1596
|
+
import { HttpRouter } from 'effect/http';
|
|
1597
|
+
import { RpcSerialization, RpcServer } from 'effect/rpc';
|
|
1554
1598
|
import { createServer } from 'node:http';
|
|
1555
1599
|
|
|
1556
1600
|
const ServerLayer = RpcServer.layerHttp({
|
|
@@ -1573,9 +1617,9 @@ Layer.launch(HttpLayer).pipe(NodeRuntime.runMain);
|
|
|
1573
1617
|
```ts
|
|
1574
1618
|
// --- client/users-client.ts ---
|
|
1575
1619
|
import { Context, Effect, Layer } from 'effect';
|
|
1576
|
-
import { FetchHttpClient } from 'effect/
|
|
1577
|
-
import { RpcClient, RpcMiddleware, RpcSerialization } from 'effect/
|
|
1578
|
-
import { RpcClientError } from 'effect/
|
|
1620
|
+
import { FetchHttpClient } from 'effect/http';
|
|
1621
|
+
import { RpcClient, RpcMiddleware, RpcSerialization } from 'effect/rpc';
|
|
1622
|
+
import { RpcClientError } from 'effect/rpc/RpcClientError';
|
|
1579
1623
|
|
|
1580
1624
|
const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
|
|
1581
1625
|
next({
|
|
@@ -1634,7 +1678,7 @@ const usersByName = Effect.gen(function*() {
|
|
|
1634
1678
|
12. **Reading `Date.now()` inside an entity or workflow handler.** Use `Clock` (and inside workflows, `DateTime.now` works because the engine wraps activities). For durable timers, use `DurableClock.sleep`.
|
|
1635
1679
|
13. **`yield* fiber` / `yield* deferred` / `yield* ref`.** Removed in v4. Use `Fiber.join`, `Deferred.await`, `Ref.get` explicitly.
|
|
1636
1680
|
14. **Treating `maxResidentEntities` like mailbox capacity.** It is a runner-wide resident-entity cap. Persisted messages wait in storage at the cap; volatile sends to new addresses fail with `MailboxFull`.
|
|
1637
|
-
15. **Implementing
|
|
1681
|
+
15. **Implementing an incomplete encoded storage driver.** `MessageStorage.Encoded` requires bounded/address-filtered `unprocessedMessages`, batched `resetAddresses`, claim-only `resetRequests`, and atomic conditional `clearReplies` when `expectedReplyId` is supplied.
|
|
1638
1682
|
|
|
1639
1683
|
## Rules
|
|
1640
1684
|
|