Client State
Client state is part of the Client.Service(...) definition. When a client connects, the actor runtime sends an initial
state value during audition. After that, client-side reducers fold incoming events into the current state.
There is no separate state service. The client itself exposes the state stream, event stream, call surface, and typed reducer helper.
Define state on the client
import { Schema as S } from "effect"
import { Client } from "liminal"
export const Player = S.Literals(["X", "O"])
export const Coordinate = S.Literals([0, 1, 2])
export const Coordinates = S.Tuple([Coordinate, Coordinate])
export class TicTacToeClient extends Client.Service<TicTacToeClient>()("examples/TicTacToeClient", {
state: {
awaitingPartner: S.Boolean,
name: Player,
},
events: {
GameStarted: {},
MoveMade: {
player: Player,
position: Coordinates,
},
GameEnded: {
winner: S.optional(Player),
},
},
external: {
Move: {
payload: S.Struct({ position: Coordinates }),
success: S.Void,
failure: S.Never,
},
},
}) {}The state record is Effect Schema struct fields. The actor runtime must hydrate a value matching that shape for every
new connection.
Hydrate on connect
In a Workerd actor runtime, hydrate returns the initial state for the connecting client.
import { Effect } from "effect"
import { TicTacToeActor } from "./TicTacToeActor.ts"
export default Effect.gen(function* () {
const { clients } = yield* TicTacToeActor
if (clients.size === 1) {
return {
awaitingPartner: true,
name: "X" as const,
}
}
yield* TicTacToeActor.others.send("GameStarted", {})
return {
awaitingPartner: false,
name: "O" as const,
}
}).pipe(Effect.orDie)Hydration is the snapshot. Events sent after hydration are deltas that reducers apply locally.
Reduce events
Reducers live next to the client runtime. Client.reducer(...) narrows the event tag and returns the reducer unchanged
at runtime.
import { Effect } from "effect"
import { TicTacToeClient } from "./TicTacToeClient.ts"
export const GameStarted = TicTacToeClient.reducer(
"GameStarted",
() =>
({ name }) =>
Effect.succeed({ name, awaitingPartner: false }),
)
export const MoveMade = TicTacToeClient.reducer("MoveMade", () => (state) => Effect.succeed(state))
export const GameEnded = TicTacToeClient.reducer("GameEnded", () => (state) => Effect.succeed(state))Each reducer receives the event first and the current state second. Return the next state to publish an update, or
return void to leave the current state unchanged.
Provide reducers
Pass the reducer table to Client.layerSocket(...).
import { BrowserSocket } from "@effect/platform-browser"
import { Layer } from "effect"
import { Atom } from "effect/unstable/reactivity"
import { Client } from "liminal"
import { TicTacToeClient } from "./TicTacToeClient.ts"
import * as reducers from "./reducers.ts"
export const runtime = Atom.runtime(
Client.layerSocket({
client: TicTacToeClient,
url: "/play",
replay: { mode: "startup" },
reducers,
}).pipe(Layer.provide(BrowserSocket.layerWebSocketConstructor)),
)Consume state
TicTacToeClient.state is a stream of hydrated and reduced state values.
import { runtime } from "./runtime"
import { TicTacToeClient } from "./TicTacToeClient.ts"
export const stateAtom = runtime.atom(TicTacToeClient.state)Read Snapshot and Delta Events for event design guidance.