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" }
50)
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)
71)
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
82)
83
84const ProductionLayer = EntitiesLayer.pipe(
85 Layer.provide(ClusterLayer)
86)
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)
92)
93
94// Finally, run your app with the entities layer.
95Layer.launch(ProductionLayer).pipe(
96 NodeRuntime.runMain
97)