refactor(chat): move actors into machine directory
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { fromCallback } from "xstate";
|
||||
|
||||
import { Logger } from "@/utils/logger";
|
||||
|
||||
import {
|
||||
readLocalHistorySnapshot,
|
||||
resolveHistoryCacheIdentity,
|
||||
syncNetworkHistory,
|
||||
} from "../../chat-history-sync";
|
||||
import type { ChatEvent } from "../../chat-events";
|
||||
|
||||
const log = new Logger("StoresChatChatHistoryFlow");
|
||||
|
||||
export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
const cacheIdentity = await resolveHistoryCacheIdentity();
|
||||
const localSnapshot = await readLocalHistorySnapshot(cacheIdentity);
|
||||
if (cancelled) return;
|
||||
sendBack({
|
||||
type: "ChatLocalHistoryLoaded",
|
||||
output: localSnapshot,
|
||||
});
|
||||
|
||||
const networkSnapshot = await syncNetworkHistory(
|
||||
localSnapshot.localCount,
|
||||
cacheIdentity,
|
||||
);
|
||||
if (cancelled || !networkSnapshot) return;
|
||||
sendBack({
|
||||
type: "ChatNetworkHistoryLoaded",
|
||||
output: networkSnapshot,
|
||||
});
|
||||
})().catch((error: unknown) => {
|
||||
if (cancelled) return;
|
||||
log.error("[chat-machine] loadHistoryActor failed", { error });
|
||||
sendBack({ type: "ChatHistoryLoadFailed", error });
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { fromCallback, fromPromise } from "xstate";
|
||||
|
||||
import { ExceptionHandler } from "@/core/errors";
|
||||
import { MessageQueue } from "@/core/net/message-queue";
|
||||
import type { ChatSendResponse } from "@/data/dto/chat";
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import type { ChatEvent } from "../../chat-events";
|
||||
import { sendResponseToUiMessage } from "../../chat-machine.helpers";
|
||||
|
||||
const log = new Logger("StoresChatChatSendFlow");
|
||||
|
||||
type UiMessage = import("@/data/dto/chat").UiMessage;
|
||||
|
||||
export const sendMessageHttpActor = fromPromise<
|
||||
{ response: ChatSendResponse; reply: UiMessage | null },
|
||||
{ content: string }
|
||||
>(async ({ input }) => {
|
||||
return sendMessageViaHttp(input.content);
|
||||
});
|
||||
|
||||
export const httpMessageQueueActor = fromCallback<ChatEvent>(
|
||||
({ sendBack, receive }) => {
|
||||
return createMessageQueueActor(sendBack, receive);
|
||||
},
|
||||
);
|
||||
|
||||
function createMessageQueueActor(
|
||||
sendBack: (event: ChatEvent) => void,
|
||||
receive: (listener: (event: ChatEvent) => void) => void,
|
||||
): () => void {
|
||||
const queue = new MessageQueue();
|
||||
|
||||
queue.setConsumer(async (content) => {
|
||||
sendBack({ type: "ChatQueuedSendStarted" });
|
||||
try {
|
||||
const output = await sendMessageViaHttp(content);
|
||||
sendBack({ type: "ChatQueuedHttpDone", output });
|
||||
} catch (error) {
|
||||
const errorMessage = ExceptionHandler.message(error);
|
||||
log.error("[chat-machine] message queue send failed", {
|
||||
error,
|
||||
});
|
||||
sendBack({
|
||||
type: "ChatQueuedSendError",
|
||||
content,
|
||||
errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
receive((event) => {
|
||||
if (event.type !== "ChatSendMessage") return;
|
||||
const content = event.content.trim();
|
||||
if (content.length === 0) return;
|
||||
queue.enqueue(content);
|
||||
});
|
||||
|
||||
return () => queue.dispose();
|
||||
}
|
||||
|
||||
async function sendMessageViaHttp(content: string): Promise<{
|
||||
response: ChatSendResponse;
|
||||
reply: UiMessage | null;
|
||||
}> {
|
||||
const chatRepo = getChatRepository();
|
||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||
const result = await chatRepo.sendMessage(content);
|
||||
if (Result.isErr(result)) {
|
||||
log.error("[chat-machine] sendMessageHttpActor failed", {
|
||||
error: result.error,
|
||||
});
|
||||
throw result.error;
|
||||
}
|
||||
if (Result.isOk(cacheIdentityResult)) {
|
||||
void chatRepo.prefetchMediaForSendResponse(
|
||||
result.data,
|
||||
cacheIdentityResult.data,
|
||||
);
|
||||
}
|
||||
const isInsufficientCredits =
|
||||
result.data.canSendMessage === false &&
|
||||
!hasRenderableSendResponse(result.data);
|
||||
return {
|
||||
response: result.data,
|
||||
reply:
|
||||
isInsufficientCredits ? null : sendResponseToUiMessage(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
function hasRenderableSendResponse(response: ChatSendResponse): boolean {
|
||||
const isLockedPaidMessage =
|
||||
response.lockDetail.locked &&
|
||||
(response.lockDetail.reason === "private_message" ||
|
||||
response.lockDetail.reason === "voice_message" ||
|
||||
response.lockDetail.reason === "image_paywall" ||
|
||||
response.lockDetail.reason === "image");
|
||||
|
||||
return (
|
||||
response.reply.trim().length > 0 ||
|
||||
(!response.lockDetail.locked && response.audioUrl.trim().length > 0) ||
|
||||
Boolean(response.image.url) ||
|
||||
isLockedPaidMessage
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { fromPromise } from "xstate";
|
||||
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import { readAndSyncHistory } from "../../chat-history-sync";
|
||||
import {
|
||||
type UnlockMessageRequest,
|
||||
type UnlockMessageOutput,
|
||||
} from "../../chat-machine.helpers";
|
||||
|
||||
const log = new Logger("StoresChatChatUnlockFlow");
|
||||
|
||||
type UiMessage = import("@/data/dto/chat").UiMessage;
|
||||
|
||||
export interface UnlockHistoryOutput {
|
||||
unlocked: boolean;
|
||||
reason: string;
|
||||
shortfallCredits: number;
|
||||
messages: UiMessage[];
|
||||
}
|
||||
|
||||
export const unlockHistoryActor = fromPromise<UnlockHistoryOutput>(async () => {
|
||||
const chatRepo = getChatRepository();
|
||||
const unlockResult = await chatRepo.unlockHistory();
|
||||
if (Result.isErr(unlockResult)) {
|
||||
log.error("[chat-machine] unlockHistoryActor failed", {
|
||||
error: unlockResult.error,
|
||||
});
|
||||
throw unlockResult.error;
|
||||
}
|
||||
|
||||
const history = await readAndSyncHistory();
|
||||
return {
|
||||
unlocked: unlockResult.data.unlocked,
|
||||
reason: unlockResult.data.reason,
|
||||
shortfallCredits: unlockResult.data.shortfallCredits,
|
||||
messages: history.messages,
|
||||
};
|
||||
});
|
||||
|
||||
export const unlockMessageActor = fromPromise<
|
||||
UnlockMessageOutput,
|
||||
UnlockMessageRequest
|
||||
>(async ({ input }) => {
|
||||
const chatRepo = getChatRepository();
|
||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||
const unlockResult = await chatRepo.unlockPrivateMessage({
|
||||
...(input.messageId ? { messageId: input.messageId } : {}),
|
||||
...(input.lockType ? { lockType: input.lockType } : {}),
|
||||
...(input.clientLockId ? { clientLockId: input.clientLockId } : {}),
|
||||
});
|
||||
if (Result.isErr(unlockResult)) {
|
||||
log.error("[chat-machine] unlockMessageActor failed", {
|
||||
messageId: input.messageId,
|
||||
clientLockId: input.clientLockId,
|
||||
error: unlockResult.error,
|
||||
});
|
||||
throw unlockResult.error;
|
||||
}
|
||||
|
||||
if (
|
||||
unlockResult.data.unlocked &&
|
||||
input.messageId &&
|
||||
!input.clientLockId &&
|
||||
Result.isOk(cacheIdentityResult)
|
||||
) {
|
||||
const markResult = await chatRepo.markPrivateMessageUnlockedInLocal(
|
||||
input.messageId,
|
||||
{
|
||||
content: unlockResult.data.content,
|
||||
audioUrl: unlockResult.data.audioUrl,
|
||||
...(unlockResult.data.image.url
|
||||
? { image: unlockResult.data.image }
|
||||
: {}),
|
||||
lockDetail: { locked: false, reason: null },
|
||||
},
|
||||
cacheIdentityResult.data,
|
||||
);
|
||||
if (Result.isErr(markResult)) {
|
||||
log.warn("[chat-machine] mark unlocked local message failed", {
|
||||
messageId: input.messageId,
|
||||
error: markResult.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayMessageId: input.displayMessageId,
|
||||
request: input,
|
||||
response: unlockResult.data,
|
||||
};
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import { setup, type ActionFunction, type EventObject } from "xstate";
|
||||
|
||||
import type { ChatEvent } from "../chat-events";
|
||||
import { loadHistoryActor } from "./actors/history";
|
||||
import {
|
||||
httpMessageQueueActor,
|
||||
loadHistoryActor,
|
||||
sendMessageHttpActor,
|
||||
} from "./actors/send";
|
||||
import {
|
||||
unlockHistoryActor,
|
||||
unlockMessageActor,
|
||||
} from "../chat-machine.actors";
|
||||
} from "./actors/unlock";
|
||||
import type { ChatState } from "../chat-state";
|
||||
|
||||
export const baseChatMachineSetup = setup({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
countLockedHistoryMessages,
|
||||
type UnlockMessageOutput,
|
||||
} from "../chat-machine.helpers";
|
||||
import type { UnlockHistoryOutput } from "../chat-unlock-flow";
|
||||
import type { UnlockHistoryOutput } from "./actors/unlock";
|
||||
import { sendMachineSetup } from "./send-flow";
|
||||
import { createChatActorActionSetup } from "./setup";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user