194 lines
7.0 KiB
TypeScript
194 lines
7.0 KiB
TypeScript
import { actor, event } from "rivetkit";
|
|
import type { CreateConversationInput, GrowActorState, GrowConversation, SetupGrowInput } from "./types.js";
|
|
import type { GrowActiveMission } from "../missions/types.js";
|
|
|
|
const buildId = (prefix: string) =>
|
|
`${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
|
|
const now = () => Date.now();
|
|
|
|
function defaultTitle(index: number) {
|
|
return index === 0 ? "Talk to Me" : `Conversation ${index + 1}`;
|
|
}
|
|
|
|
function ensureInitialized(state: GrowActorState) {
|
|
if (!state.userId) throw new Error("Grow actor is not initialized");
|
|
}
|
|
|
|
function normalizeState(state: GrowActorState) {
|
|
state.conversations ??= [];
|
|
state.activeConversationId ??= null;
|
|
state.activeMissions ??= [];
|
|
state.createdAt ??= now();
|
|
state.updatedAt ??= now();
|
|
}
|
|
|
|
function createConversationRecord(state: GrowActorState, input: CreateConversationInput = {}): GrowConversation {
|
|
const timestamp = now();
|
|
return {
|
|
id: buildId("conversation"),
|
|
title: input.title?.trim() || defaultTitle(state.conversations.length),
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
};
|
|
}
|
|
|
|
export const growActor = actor({
|
|
state: {
|
|
userId: "",
|
|
conversations: [],
|
|
activeConversationId: null,
|
|
activeMissions: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
} as GrowActorState,
|
|
|
|
events: {
|
|
ready: event<{ userId: string; conversationId: string }>(),
|
|
conversationCreated: event<GrowConversation>(),
|
|
conversationReset: event<GrowConversation>(),
|
|
missionActivated: event<GrowActiveMission>(),
|
|
missionUpdated: event<GrowActiveMission>(),
|
|
},
|
|
|
|
actions: {
|
|
setup: async (c, input: SetupGrowInput) => {
|
|
if (c.state.userId && c.state.userId !== input.userId) {
|
|
throw new Error("Grow actor already bound to a different user");
|
|
}
|
|
|
|
normalizeState(c.state);
|
|
c.state.userId = input.userId;
|
|
if (!c.state.conversations.length) {
|
|
const conversation = createConversationRecord(c.state, { title: "Talk to Me" });
|
|
c.state.conversations.push(conversation);
|
|
c.state.activeConversationId = conversation.id;
|
|
c.broadcast("conversationCreated", conversation);
|
|
} else if (!c.state.activeConversationId) {
|
|
c.state.activeConversationId = c.state.conversations[0]?.id ?? null;
|
|
}
|
|
|
|
const activeConversationId = c.state.activeConversationId;
|
|
if (!activeConversationId) throw new Error("Grow actor has no active conversation");
|
|
|
|
c.state.updatedAt = now();
|
|
c.broadcast("ready", {
|
|
userId: c.state.userId,
|
|
conversationId: activeConversationId,
|
|
});
|
|
|
|
return {
|
|
userId: c.state.userId,
|
|
activeConversationId: c.state.activeConversationId,
|
|
conversations: c.state.conversations,
|
|
activeMissions: c.state.activeMissions,
|
|
};
|
|
},
|
|
|
|
getState: async (c) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
return {
|
|
userId: c.state.userId,
|
|
activeConversationId: c.state.activeConversationId,
|
|
conversations: c.state.conversations,
|
|
activeMissions: c.state.activeMissions,
|
|
};
|
|
},
|
|
|
|
createConversation: async (c, input: CreateConversationInput = {}) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
const conversation = createConversationRecord(c.state, input);
|
|
c.state.conversations.unshift(conversation);
|
|
c.state.activeConversationId = conversation.id;
|
|
c.state.updatedAt = conversation.updatedAt;
|
|
c.broadcast("conversationCreated", conversation);
|
|
return conversation;
|
|
},
|
|
|
|
listConversations: async (c) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
return c.state.conversations;
|
|
},
|
|
|
|
getConversation: async (c, input: { conversationId: string }) => {
|
|
ensureInitialized(c.state);
|
|
return c.state.conversations.find((conversation) => conversation.id === input.conversationId) ?? null;
|
|
},
|
|
|
|
touchConversation: async (c, input: { conversationId: string; title?: string }) => {
|
|
ensureInitialized(c.state);
|
|
const conversation = c.state.conversations.find((item) => item.id === input.conversationId);
|
|
if (!conversation) throw new Error(`Unknown conversation: ${input.conversationId}`);
|
|
if (input.title?.trim()) conversation.title = input.title.trim();
|
|
conversation.updatedAt = now();
|
|
c.state.activeConversationId = conversation.id;
|
|
c.state.updatedAt = conversation.updatedAt;
|
|
return conversation;
|
|
},
|
|
|
|
resetConversation: async (c, input: { conversationId?: string; title?: string } = {}) => {
|
|
ensureInitialized(c.state);
|
|
const conversationId = input.conversationId ?? c.state.activeConversationId;
|
|
if (!conversationId) {
|
|
const created = createConversationRecord(c.state, { title: input.title ?? "Talk to Me" });
|
|
c.state.conversations.unshift(created);
|
|
c.state.activeConversationId = created.id;
|
|
c.state.updatedAt = created.updatedAt;
|
|
c.broadcast("conversationCreated", created);
|
|
return created;
|
|
}
|
|
|
|
const conversation = c.state.conversations.find((item) => item.id === conversationId);
|
|
if (!conversation) throw new Error(`Unknown conversation: ${conversationId}`);
|
|
conversation.title = input.title?.trim() || conversation.title;
|
|
conversation.updatedAt = now();
|
|
c.state.activeConversationId = conversation.id;
|
|
c.state.updatedAt = conversation.updatedAt;
|
|
c.broadcast("conversationReset", conversation);
|
|
return conversation;
|
|
},
|
|
|
|
registerActiveMission: async (c, input: GrowActiveMission) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
const existingIndex = c.state.activeMissions.findIndex((mission) => mission.instanceId === input.instanceId);
|
|
const record = { ...input, updatedAt: now() };
|
|
if (existingIndex >= 0) {
|
|
c.state.activeMissions[existingIndex] = record;
|
|
c.broadcast("missionUpdated", record);
|
|
} else {
|
|
c.state.activeMissions.unshift(record);
|
|
c.broadcast("missionActivated", record);
|
|
}
|
|
c.state.updatedAt = record.updatedAt;
|
|
return record;
|
|
},
|
|
|
|
updateActiveMission: async (c, input: Pick<GrowActiveMission, "instanceId"> & Partial<GrowActiveMission>) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
const mission = c.state.activeMissions.find((item) => item.instanceId === input.instanceId);
|
|
if (!mission) throw new Error(`Unknown active mission: ${input.instanceId}`);
|
|
Object.assign(mission, input, { updatedAt: now() });
|
|
c.state.updatedAt = mission.updatedAt;
|
|
c.broadcast("missionUpdated", mission);
|
|
return mission;
|
|
},
|
|
|
|
listActiveMissions: async (c) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
return c.state.activeMissions;
|
|
},
|
|
|
|
getActiveMission: async (c, input: { instanceId: string }) => {
|
|
normalizeState(c.state);
|
|
ensureInitialized(c.state);
|
|
return c.state.activeMissions.find((mission) => mission.instanceId === input.instanceId) ?? null;
|
|
},
|
|
},
|
|
});
|