import { Agent, type Connection, type WSMessage } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";
type Message = { id: string; from: string; text: string; at: number };
type State = { messages: Message[]; members: string[] };
type ClientFrame =
| { kind: "hello"; name: string }
| { kind: "say"; text: string }
| { kind: "typing"; on: boolean };
export default class RoomAgent extends Agent<GeneratedEnv, State> {
override initialState: State = { messages: [], members: [] };
override async onConnect(conn: Connection) {
conn.setState({ name: null }); // anonymous until `hello`
}
override async onMessage(conn: Connection, message: WSMessage) {
if (typeof message !== "string") return;
const frame = JSON.parse(message) as ClientFrame;
const name = (conn.state as { name: string | null } | null)?.name;
switch (frame.kind) {
case "hello": {
conn.setState({ name: frame.name });
this.refreshMembers();
break;
}
case "say": {
if (!name) return;
const msg = { id: crypto.randomUUID(), from: name, text: frame.text, at: Date.now() };
this.setState({ ...this.state, messages: [...this.state.messages, msg].slice(-100) });
break;
}
case "typing": {
if (!name) return;
// Transient — broadcast, don't persist. Skip the sender by passing [conn.id].
this.broadcast(JSON.stringify({ kind: "typing", from: name, on: frame.on }), [conn.id]);
break;
}
}
}
override async onClose() { this.refreshMembers(); }
private refreshMembers() {
const names = new Set<string>();
for (const c of this.getConnections()) {
const n = (c.state as { name: string | null } | null)?.name;
if (n) names.add(n);
}
const members = Array.from(names).sort();
this.setState({ ...this.state, members });
}
override async onRequest() { return Response.json({ instance: this.name, ...this.state }); }
}