Skip to content
Source
Sign inSign up

10_entities.ts

97 lines3.2 KB

1/**
2 * @title Defining cluster entities
3 *
4 * Define distributed entity RPCs and run them in a cluster.
5 */
6import { NodeClusterSocket, NodeRuntime } from "@effect/platform-node"
7import { Effect, Layer, Ref, Schema } from "effect"
8import { ClusterSchema, Entity, TestRunner } from "effect/cluster"
9import { Rpc } from "effect/rpc"
10import type { SqlClient } from "effect/sql"
11​
12export const Increment = Rpc.make("Increment", {
13 payload: { amount: Schema.Int },
14 success: Schema.Int
15})
16​
17export const GetCount = Rpc.make("GetCount", {
18 success: Schema.Int
19})
20 // If you want GetCount messages to be persisted, you can annotate the RPC
21 // schema with `ClusterSchema.Persisted`.
22 //
23 // By default, messages are volatile and only sent over a network.
24 .annotate(ClusterSchema.Persisted, true)
25​
26// `Entity.make` takes an array of Rpc definitions
27export const Counter = Entity.make("Counter", [Increment, GetCount])
28​
29// Entity handlers can keep in-memory state while the entity is active.
30// `maxIdleTime` controls passivation: if the entity is idle long enough, it is
31// stopped and later recreated on demand.
32export const CounterEntityLayer = Counter.toLayer(
33 Effect.gen(function*() {
34 const count = yield* Ref.make(0)
35​
36 return Counter.of({
37 Increment: ({ payload }) => Ref.updateAndGet(count, (current) => current + payload.amount),
38 GetCount: () =>
39 Ref.get(count).pipe(
40 // Add Rpc.fork to allow the GetCount handler to run concurrently with
41 // Increment handlers.
42 //
43 // This opts-out of the default behavior where all handlers for a
44 // given entity run sequentially.
45 Rpc.fork
46 )
47 })
48 }),
49 { maxIdleTime: "5 minutes" }
51​
52// If you ever need to access an entity client, you can use the `client`
53// property on the entity definition.
54export const useCounter = Effect.gen(function*() {
55 const clientFor = yield* Counter.client
56 const counter = clientFor("counter-123")
57​
58 const afterIncrement = yield* counter.Increment({ amount: 1 })
59 const currentCount = yield* counter.GetCount()
60​
61 console.log(`Count after increment: ${afterIncrement}, current count: ${currentCount}`)
62})
63​
64// `SingleRunner.layer` is useful for local development / tests where you still
65// want the cluster entity runtime model.
66declare const SqlClientLayer: Layer.Layer<SqlClient.SqlClient>
67​
68// Create the cluster layer using `NodeClusterSocket.layer`
69const ClusterLayer = NodeClusterSocket.layer().pipe(
70 Layer.provide(SqlClientLayer)
72​
73// You can also use `TestRunner.layer` to run your entities in a single process,
74// without any network communication and in-memory storage. This is useful for testing and
75// development.
76const ClusterLayerTest = TestRunner.layer
77​
78// Merge all your entity layers together and provide the cluster layer to run
79// them in a cluster.
80const EntitiesLayer = Layer.mergeAll(
81 CounterEntityLayer
83​
84const ProductionLayer = EntitiesLayer.pipe(
85 Layer.provide(ClusterLayer)
87​
88export const TestLayer = EntitiesLayer.pipe(
89 // For testing, we can use `Layer.provideMerge` to tests can access storage
90 // and other cluster services directly.
91 Layer.provideMerge(ClusterLayerTest)
93​
94// Finally, run your app with the entities layer.
95Layer.launch(ProductionLayer).pipe(
96 NodeRuntime.runMain