Skip to content

Cluster.Entity reference

Source: packages/effect-cluster/src/Entity.ts

An Entity in an Effect Cluster: a stateful, addressable instance per entity id, whose messages are typed by its RPCs. Runners place each instance on one runner and deliver its messages one at a time.

Declare it from an Effect Cluster entity with its implementation, and the class is both what callers yield* and what deploys it. Or declare it bare, like Effect Cluster’s Entity.make, from a type and its RPCs, and implement it with .make(props, impl), as a Layer: callers import the bare declaration without bundling the implementation or its pool.

The implementation has two phases. The outer Effect runs once per runner and resolves bindings. The inner Effect runs once per instance, on its first message, and returns its handlers, typed by the entity’s .of. Providing a pool Layer to the implementation, such as ClusterDocker.SwarmPool("Default"), is what deploys it.

yield* Counter from any runtime returns the entity’s namespace, like a Durable Object’s: getByName(entityId) returns a client with the same shape as Effect Cluster’s Entity.client(entityId). Inside the cluster, calls go through Effect Cluster directly; elsewhere they go through the pool’s gateway over HTTP, so callers need no database access and no route to the pool.

src/Counter.ts
const Counters = Entity.make("Counter", [
Rpc.make("Increment", { payload: { amount: Schema.Int }, success: Schema.Int }),
]);
export default class Counter extends Cluster.Entity<Counter>()(
Counters,
{ main: import.meta.url },
Effect.gen(function* () {
return Effect.gen(function* () {
const count = yield* Ref.make(0);
return Counters.of({
Increment: ({ payload }) => Ref.updateAndGet(count, (n) => n + payload.amount),
});
});
}).pipe(Effect.provide(Default)),
) {}

Entity: Keeping its code out of its callers

Section titled “Entity: Keeping its code out of its callers”

A bare declaration, which callers import

src/Counter.ts
export const Increment = Rpc.make("Increment", {
payload: { amount: Schema.Int },
success: Schema.Int,
});
export class Counter extends Cluster.Entity<Counter>()("Counter", [Increment]) {}

An existing Effect Cluster entity

import { Counter as CounterEntity } from "./entities/counter.ts";
export class Counter extends Cluster.Entity<Counter>()(CounterEntity) {}

Its implementation, as a Layer, deployed to a pool

src/CounterLive.ts
export default Counter.make(
{ main: import.meta.url },
Effect.gen(function* () {
return Effect.gen(function* () {
const count = yield* Ref.make(0);
return Counter.of({
Increment: ({ payload }) => Ref.updateAndGet(count, (n) => n + payload.amount),
});
});
}).pipe(Effect.provide(Default)),
);

Middleware, provided with the pool

export const Increment = Rpc.make("Increment", {
payload: { amount: Schema.Int },
success: Schema.Int,
}).middleware(Auth);
export default Counter.make(
{ main: import.meta.url },
Effect.gen(function* () {
return Effect.gen(function* () {
return Counter.of({
Increment: () => Effect.map(CurrentUser, (user) => user.id.length),
});
});
}).pipe(Effect.provide(Default.pipe(Layer.provide(AuthLive)))),
);

An existing Effect Cluster program, run as it is

import { Tally as TallyEntity } from "./entities/tally.ts";
export const Tally = Cluster.Entity(TallyEntity, {
main: "./entities/tally.ts",
program: "./runner.ts",
}).pipe(Effect.provide(Legacy));

From any runtime

const counters = yield* Counter;
const counter = yield* counters.getByName("visits");
const count = yield* counter.Increment({ amount: 1 });

Without waiting for the reply

yield* counter.Increment({ amount: 1 }, { discard: true });

Through client middleware

const counter = yield* counters.getByName("visits");
yield* counter.Increment({ amount: 1 }).pipe(Effect.provide(AuthClient));