Live queries
A client can subscribe to a query. It gets the current answer first, then a new one each time a command changes it. Nothing polls: the host is the only writer of an actor, so it knows which tables each command wrote, and it runs a subscribed query again only when one of those tables is a table the query read.
Subscribe
Every query on a client has subscribe, which takes the query's payload and returns a Stream. Each update is { value, version }, where version counts the actor's commands that the value includes.
This page uses one actor:
import { Schema } from "effect"
import { Akter } from "@akterjs/effect/contract"
export const Room = Akter.make("Room", {
key: Schema.String,
state: Schema.Struct({ title: Schema.String }),
initial: () => ({ title: "Untitled" }),
commands: {
post: Akter.command({ payload: { text: Schema.String }, success: Schema.Void }),
rename: Akter.command({ payload: { title: Schema.String }, success: Schema.Void }),
},
queries: {
recent: Akter.query({ payload: { limit: Schema.Int }, success: Schema.Array(Schema.String) }),
},
})import { Effect } from "effect"
import { Akter } from "@akterjs/effect/contract"
import { Room } from "./room.contract.ts"
export const RoomLive = Room.toLayer({
migrations: {
1: (sql) => sql`CREATE TABLE messages (seq INTEGER PRIMARY KEY, text TEXT NOT NULL)`,
},
commands: {
post: (_room, { text }) => Effect.flatMap(Akter.sql, (sql) => sql`INSERT INTO messages ${sql.insert({ text })}`),
rename: (_room, { title }) => Room.update({ title }),
},
queries: {
recent: (_room, { limit }) =>
Effect.flatMap(Akter.sql, (sql) =>
Effect.map(
sql<{ text: string }>`SELECT text FROM messages ORDER BY seq DESC LIMIT ${limit}`,
(rows) => rows.map((row) => row.text),
),
),
},
})This script serves the room on the local engine, subscribes to recent, and posts a message after each update. Before the second post it renames the room:
import { Effect, Layer, Stream } from "effect"
import { HttpServer } from "effect/http"
import { Akter } from "@akterjs/effect"
import { akterClient } from "@akterjs/client/effect"
import { Room } from "./room.contract.ts"
import { RoomLive } from "./room.actor.ts"
const server = Akter.serve({ actors: [Room], port: 0 }).pipe(
Layer.provide(Akter.local(".akter").pipe(Layer.provide(RoomLive))),
)
const client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))
const program = Effect.gen(function* () {
const room = yield* Room.client("lobby")
yield* room.recent.subscribe({ limit: 2 }).pipe(
Stream.take(3),
Stream.zipWithIndex,
Stream.runForEach(([{ value, version }, index]) =>
Effect.gen(function* () {
console.log(`version ${version}: [${value.join(", ")}]`)
if (index === 0) yield* room.post({ text: "hi" })
if (index === 1) {
yield* room.rename({ title: "Lunch" })
yield* room.post({ text: "lunch?" })
}
}),
),
)
})
Effect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))version 0: []
version 1: [hi]
version 3: [lunch?, hi]There's no update at version 2. rename changed only the room's state, which recent doesn't read, so the host didn't run the query again.
What you can rely on
- Every value comes from committed commands. A subscriber never sees a change that could still roll back.
- A command that changes the answer always sends an update. A command that leaves it the same sends none, so two updates in a row always differ.
room.stateis a Stream of the actor's state that works the same way: the current state first, then a new value each time a command changes it.