branch:
room-agent.ts
4234 bytesRaw
import {
callable,
getAgentByName,
type Connection,
type ConnectionContext
} from "agents";
import { PlaygroundAgent as Agent } from "../../shared/playground-agent";
import type { LobbyAgent } from "./lobby-agent";
export interface RoomMember {
userId: string;
joinedAt: string;
}
export interface ChatMessage {
id: string;
userId: string;
text: string;
timestamp: string;
}
export interface RoomState {
roomId: string;
members: Record<string, RoomMember>;
messages: ChatMessage[];
createdAt: string;
}
export class RoomAgent extends Agent<Env, RoomState> {
initialState: RoomState = {
roomId: "",
members: {},
messages: [],
createdAt: ""
};
// Track WebSocket connections to user IDs
private connectionToUser: Map<string, string> = new Map();
onConnect(connection: Connection, ctx: ConnectionContext) {
super.onConnect(connection, ctx);
console.log(`Connection to room: ${connection.id}`);
}
onClose(connection: Connection) {
super.onClose(connection);
// Auto-leave when connection closes
const odesc = this.connectionToUser.get(connection.id);
if (odesc) {
this.leaveInternal(odesc);
this.connectionToUser.delete(connection.id);
}
}
@callable({ description: "Initialize the room" })
async initRoom(roomId: string): Promise<RoomState> {
if (!this.state.roomId) {
this.setState({
...this.state,
roomId,
createdAt: new Date().toISOString()
});
}
return this.state;
}
@callable({ description: "Join the room" })
async join(userId: string): Promise<RoomState> {
if (!this.state.members[userId]) {
const newMembers = {
...this.state.members,
[userId]: {
userId,
joinedAt: new Date().toISOString()
}
};
this.setState({ ...this.state, members: newMembers });
// Notify lobby of member count change
await this.notifyLobby();
// Broadcast join event
this.broadcast(
JSON.stringify({
type: "member_joined",
userId,
memberCount: Object.keys(newMembers).length
})
);
}
return this.state;
}
@callable({ description: "Leave the room" })
async leave(userId: string): Promise<RoomState> {
return this.leaveInternal(userId);
}
private async leaveInternal(userId: string): Promise<RoomState> {
if (this.state.members[userId]) {
const { [userId]: _, ...remainingMembers } = this.state.members;
this.setState({ ...this.state, members: remainingMembers });
// Notify lobby of member count change
await this.notifyLobby();
// Broadcast leave event
this.broadcast(
JSON.stringify({
type: "member_left",
userId,
memberCount: Object.keys(remainingMembers).length
})
);
}
return this.state;
}
@callable({ description: "Send a message to the room" })
sendMessage(userId: string, text: string): ChatMessage {
const message: ChatMessage = {
id: crypto.randomUUID(),
userId,
text,
timestamp: new Date().toISOString()
};
// Keep last 100 messages
const messages = [...this.state.messages, message].slice(-100);
this.setState({ ...this.state, messages });
// Broadcast the message
this.broadcast(
JSON.stringify({
type: "chat_message",
message
})
);
return message;
}
@callable({ description: "Get room members" })
getMembers(): string[] {
return Object.keys(this.state.members);
}
@callable({ description: "Get recent messages" })
getMessages(limit = 50): ChatMessage[] {
return this.state.messages.slice(-limit);
}
@callable({ description: "Get member count" })
getMemberCount(): number {
return Object.keys(this.state.members).length;
}
private async notifyLobby() {
try {
const lobby = await getAgentByName<Env, LobbyAgent>(
this.env.LobbyAgent,
"main"
);
await lobby.updateRoomCount(
this.state.roomId,
Object.keys(this.state.members).length
);
} catch (e) {
console.error("Failed to notify lobby:", e);
}
}
}