From 3b53fc9bad7b5ae60ba0f9f8958d6366deb63b1c Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 6 Jul 2026 15:13:19 +0800 Subject: [PATCH] refactor(chat): export flow capabilities individually --- src/stores/chat/chat-history-flow.ts | 95 ++++++------ src/stores/chat/chat-machine.actors.ts | 3 - src/stores/chat/chat-machine.ts | 66 +++++++-- src/stores/chat/chat-media-flow.ts | 14 +- src/stores/chat/chat-send-flow.ts | 70 ++++----- src/stores/chat/chat-unlock-flow.ts | 194 +++++++++++++------------ 6 files changed, 245 insertions(+), 197 deletions(-) diff --git a/src/stores/chat/chat-history-flow.ts b/src/stores/chat/chat-history-flow.ts index 869a2068..06d8832d 100644 --- a/src/stores/chat/chat-history-flow.ts +++ b/src/stores/chat/chat-history-flow.ts @@ -17,58 +17,53 @@ const log = new Logger("StoresChatChatHistoryFlow"); type UiMessage = import("@/data/dto/chat").UiMessage; -export const chatHistoryActors = { - loadHistory: fromCallback(({ sendBack }) => { - let cancelled = false; +export const loadHistoryActor = fromCallback(({ sendBack }) => { + let cancelled = false; - void (async () => { - const localSnapshot = await readLocalHistorySnapshot(); - if (cancelled) return; - sendBack({ - type: "ChatLocalHistoryLoaded", - output: localSnapshot, - }); - - const networkSnapshot = await syncNetworkHistory( - localSnapshot.localCount, - ); - 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 }); + void (async () => { + const localSnapshot = await readLocalHistorySnapshot(); + if (cancelled) return; + sendBack({ + type: "ChatLocalHistoryLoaded", + output: localSnapshot, }); - return () => { - cancelled = true; - }; - }), + const networkSnapshot = await syncNetworkHistory( + localSnapshot.localCount, + ); + 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 }); + }); - loadMoreHistory: fromPromise< - { messages: UiMessage[]; hasMore: boolean; newOffset: number }, - { offset: number } - >(async ({ input }) => { - const chatRepo = getChatRepository(); - const result = await chatRepo.getHistory(PAGE_SIZE, input.offset); - if (Result.isErr(result)) { - log.error("[chat-machine] loadMoreHistoryActor failed", { - error: result.error, - }); - throw result.error; - } - const page = localMessagesToUi(result.data.messages); - void chatRepo.prefetchMediaForMessages(result.data.messages); - return { - messages: page, - hasMore: page.length >= PAGE_SIZE, - newOffset: input.offset + page.length, - }; - }), -}; + return () => { + cancelled = true; + }; +}); -export const loadHistoryActor = chatHistoryActors.loadHistory; -export const loadMoreHistoryActor = chatHistoryActors.loadMoreHistory; +export const loadMoreHistoryActor = fromPromise< + { messages: UiMessage[]; hasMore: boolean; newOffset: number }, + { offset: number } +>(async ({ input }) => { + const chatRepo = getChatRepository(); + const result = await chatRepo.getHistory(PAGE_SIZE, input.offset); + if (Result.isErr(result)) { + log.error("[chat-machine] loadMoreHistoryActor failed", { + error: result.error, + }); + throw result.error; + } + const page = localMessagesToUi(result.data.messages); + void chatRepo.prefetchMediaForMessages(result.data.messages); + return { + messages: page, + hasMore: page.length >= PAGE_SIZE, + newOffset: input.offset + page.length, + }; +}); diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts index 7e57e31a..fb95a382 100644 --- a/src/stores/chat/chat-machine.actors.ts +++ b/src/stores/chat/chat-machine.actors.ts @@ -7,17 +7,14 @@ * - chat-unlock-flow */ export { - chatHistoryActors, loadHistoryActor, loadMoreHistoryActor, } from "./chat-history-flow"; export { - chatSendActors, httpMessageQueueActor, sendMessageHttpActor, } from "./chat-send-flow"; export { - chatUnlockActors, unlockHistoryActor, unlockMessageActor, type UnlockMessageOutput, diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 5e3a77b7..bace480e 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -39,10 +39,38 @@ import { shouldAutoUnlockHistory, shouldPromptUnlockHistory, } from "./chat-machine.helpers"; -import { chatHistoryActors } from "./chat-history-flow"; -import { chatMediaActions } from "./chat-media-flow"; -import { chatSendActions, chatSendActors } from "./chat-send-flow"; -import { chatUnlockActions, chatUnlockActors } from "./chat-unlock-flow"; +import { + loadHistoryActor, + loadMoreHistoryActor, +} from "./chat-history-flow"; +import { + appendGuestUserImageAction, + appendUserImageAction, +} from "./chat-media-flow"; +import { + appendGuestUserMessageAction, + appendQueuedSendErrorMessageAction, + appendUserMessageAction, + applyQueuedHttpOutputAction, + clearUpgradePromptAction, + httpMessageQueueActor, + markQueuedSendStartedAction, + sendMessageHttpActor, +} from "./chat-send-flow"; +import { + applyUnlockMessageSucceededAction, + clearUnlockPaywallRequestAction, + dismissUnlockHistoryPromptAction, + markPaymentUnlockPendingAction, + markUnlockHistoryFailedAction, + markUnlockHistoryStartedAction, + markUnlockMessageStartedAction, + requestUnlockPaymentFromErrorAction, + requestUnlockPaymentFromOutputAction, + showUnlockHistoryPromptAction, + unlockHistoryActor, + unlockMessageActor, +} from "./chat-unlock-flow"; // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 export type { ChatState } from "./chat-state"; @@ -58,14 +86,32 @@ export const chatMachine = setup({ events: {} as ChatEvent, }, actors: { - ...chatHistoryActors, - ...chatSendActors, - ...chatUnlockActors, + loadHistory: loadHistoryActor, + loadMoreHistory: loadMoreHistoryActor, + sendMessageHttp: sendMessageHttpActor, + httpMessageQueue: httpMessageQueueActor, + unlockHistory: unlockHistoryActor, + unlockMessage: unlockMessageActor, }, actions: { - ...chatSendActions, - ...chatMediaActions, - ...chatUnlockActions, + appendGuestUserMessage: appendGuestUserMessageAction, + appendUserMessage: appendUserMessageAction, + appendQueuedSendErrorMessage: appendQueuedSendErrorMessageAction, + markQueuedSendStarted: markQueuedSendStartedAction, + applyQueuedHttpOutput: applyQueuedHttpOutputAction, + clearUpgradePrompt: clearUpgradePromptAction, + appendGuestUserImage: appendGuestUserImageAction, + appendUserImage: appendUserImageAction, + markPaymentUnlockPending: markPaymentUnlockPendingAction, + showUnlockHistoryPrompt: showUnlockHistoryPromptAction, + dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction, + markUnlockHistoryStarted: markUnlockHistoryStartedAction, + markUnlockHistoryFailed: markUnlockHistoryFailedAction, + markUnlockMessageStarted: markUnlockMessageStartedAction, + applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction, + requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction, + requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction, + clearUnlockPaywallRequest: clearUnlockPaywallRequestAction, enqueueMessage: sendTo("messageQueue", ({ event }) => event), startGuestSession: assign(() => ({ diff --git a/src/stores/chat/chat-media-flow.ts b/src/stores/chat/chat-media-flow.ts index 7a76cb06..3a0ef2c0 100644 --- a/src/stores/chat/chat-media-flow.ts +++ b/src/stores/chat/chat-media-flow.ts @@ -5,8 +5,8 @@ import { beginPendingReply } from "./chat-machine.helpers"; const log = new Logger("StoresChatChatMediaFlow"); -export const chatMediaActions = { - appendGuestUserImage: chatAssign(({ context, event }: ChatActionArgs) => { +export const appendGuestUserImageAction = chatAssign( + ({ context, event }: ChatActionArgs) => { if (event.type !== "ChatSendImage") return {}; const today = todayString(); @@ -28,9 +28,11 @@ export const chatMediaActions = { upgradePromptVisible: false, upgradeReason: null, }; - }), + }, +); - appendUserImage: chatAssign(({ context, event }: ChatActionArgs) => { +export const appendUserImageAction = chatAssign( + ({ context, event }: ChatActionArgs) => { if (event.type !== "ChatSendImage") return {}; const today = todayString(); @@ -52,5 +54,5 @@ export const chatMediaActions = { upgradePromptVisible: false, upgradeReason: null, }; - }), -}; + }, +); diff --git a/src/stores/chat/chat-send-flow.ts b/src/stores/chat/chat-send-flow.ts index 33d09434..18dcef56 100644 --- a/src/stores/chat/chat-send-flow.ts +++ b/src/stores/chat/chat-send-flow.ts @@ -23,21 +23,21 @@ const log = new Logger("StoresChatChatSendFlow"); type UiMessage = import("@/data/dto/chat").UiMessage; -export const chatSendActors = { - sendMessageHttp: fromPromise< - { response: ChatSendResponse; reply: UiMessage | null }, - { content: string } - >(async ({ input }) => { - return sendMessageViaHttp(input.content); - }), +export const sendMessageHttpActor = fromPromise< + { response: ChatSendResponse; reply: UiMessage | null }, + { content: string } +>(async ({ input }) => { + return sendMessageViaHttp(input.content); +}); - httpMessageQueue: fromCallback(({ sendBack, receive }) => { +export const httpMessageQueueActor = fromCallback( + ({ sendBack, receive }) => { return createMessageQueueActor(sendBack, receive); - }), -}; + }, +); -export const chatSendActions = { - appendGuestUserMessage: chatAssign(({ context, event }: ChatActionArgs) => { +export const appendGuestUserMessageAction = chatAssign( + ({ context, event }: ChatActionArgs) => { if (event.type !== "ChatSendMessage") return {}; const today = todayString(); @@ -60,9 +60,11 @@ export const chatSendActions = { upgradePromptVisible: false, upgradeReason: null, }; - }), + }, +); - appendUserMessage: chatAssign(({ context, event }: ChatActionArgs) => { +export const appendUserMessageAction = chatAssign( + ({ context, event }: ChatActionArgs) => { if (event.type !== "ChatSendMessage") return {}; const today = todayString(); @@ -84,9 +86,11 @@ export const chatSendActions = { upgradePromptVisible: false, upgradeReason: null, }; - }), + }, +); - appendQueuedSendErrorMessage: chatAssign(({ context }: ChatContextArgs) => { +export const appendQueuedSendErrorMessageAction = chatAssign( + ({ context }: ChatContextArgs) => { const messages = [ ...context.messages, { @@ -96,29 +100,29 @@ export const chatSendActions = { }, ]; return { messages, ...finishPendingReply(context) }; - }), + }, +); - markQueuedSendStarted: chatAssign(({ context }: ChatContextArgs) => +export const markQueuedSendStartedAction = chatAssign( + ({ context }: ChatContextArgs) => beginPendingReply(context), - ), +); - applyQueuedHttpOutput: chatAssign(({ context, event }: ChatActionArgs) => { +export const applyQueuedHttpOutputAction = chatAssign( + ({ context, event }: ChatActionArgs) => { if (event.type !== "ChatQueuedHttpDone") return {}; return applyHttpSendOutput(context, event.output); - }), + }, +); - clearUpgradePrompt: chatAssign(() => ({ - upgradePromptVisible: false, - upgradeReason: null, - canSendMessage: true, - cannotSendReason: null, - requiredCredits: 0, - shortfallCredits: 0, - })), -}; - -export const sendMessageHttpActor = chatSendActors.sendMessageHttp; -export const httpMessageQueueActor = chatSendActors.httpMessageQueue; +export const clearUpgradePromptAction = chatAssign(() => ({ + upgradePromptVisible: false, + upgradeReason: null, + canSendMessage: true, + cannotSendReason: null, + requiredCredits: 0, + shortfallCredits: 0, +})); function createMessageQueueActor( sendBack: (event: ChatEvent) => void, diff --git a/src/stores/chat/chat-unlock-flow.ts b/src/stores/chat/chat-unlock-flow.ts index e3e39cd3..c50a55be 100644 --- a/src/stores/chat/chat-unlock-flow.ts +++ b/src/stores/chat/chat-unlock-flow.ts @@ -19,101 +19,102 @@ const log = new Logger("StoresChatChatUnlockFlow"); type UiMessage = import("@/data/dto/chat").UiMessage; -export const chatUnlockActors = { - unlockHistory: fromPromise<{ - unlocked: boolean; - reason: string; - shortfallCredits: number; - messages: UiMessage[]; - hasMore: boolean; - newOffset: number; - }>(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; - } +export const unlockHistoryActor = fromPromise<{ + unlocked: boolean; + reason: string; + shortfallCredits: number; + messages: UiMessage[]; + hasMore: boolean; + newOffset: number; +}>(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, - hasMore: history.hasMore, - newOffset: history.newOffset, - }; - }), + const history = await readAndSyncHistory(); + return { + unlocked: unlockResult.data.unlocked, + reason: unlockResult.data.reason, + shortfallCredits: unlockResult.data.shortfallCredits, + messages: history.messages, + hasMore: history.hasMore, + newOffset: history.newOffset, + }; +}); - unlockMessage: fromPromise( - async ({ input }) => { - const chatRepo = getChatRepository(); - const unlockResult = await chatRepo.unlockPrivateMessage(input.messageId); - if (Result.isErr(unlockResult)) { - log.error("[chat-machine] unlockMessageActor failed", { - messageId: input.messageId, - error: unlockResult.error, - }); - throw unlockResult.error; - } +export const unlockMessageActor = fromPromise< + UnlockMessageOutput, + { messageId: string } +>(async ({ input }) => { + const chatRepo = getChatRepository(); + const unlockResult = await chatRepo.unlockPrivateMessage(input.messageId); + if (Result.isErr(unlockResult)) { + log.error("[chat-machine] unlockMessageActor failed", { + messageId: input.messageId, + error: unlockResult.error, + }); + throw unlockResult.error; + } - if (unlockResult.data.unlocked) { - const markResult = await chatRepo.markPrivateMessageUnlockedInLocal( - input.messageId, - unlockResult.data.lockDetail, - ); - if (Result.isErr(markResult)) { - log.warn("[chat-machine] mark unlocked local message failed", { - messageId: input.messageId, - error: markResult.error, - }); - } - } - - return { + if (unlockResult.data.unlocked) { + const markResult = await chatRepo.markPrivateMessageUnlockedInLocal( + input.messageId, + unlockResult.data.lockDetail, + ); + if (Result.isErr(markResult)) { + log.warn("[chat-machine] mark unlocked local message failed", { messageId: input.messageId, - response: unlockResult.data, - }; - }, - ), -}; + error: markResult.error, + }); + } + } -export const chatUnlockActions = { - markPaymentUnlockPending: chatAssign(() => ({ - paymentUnlockPending: true, - unlockHistoryError: null, - })), + return { + messageId: input.messageId, + response: unlockResult.data, + }; +}); - showUnlockHistoryPrompt: chatAssign(({ context }: ChatContextArgs) => ({ +export const markPaymentUnlockPendingAction = chatAssign(() => ({ + paymentUnlockPending: true, + unlockHistoryError: null, +})); + +export const showUnlockHistoryPromptAction = chatAssign( + ({ context }: ChatContextArgs) => ({ paymentUnlockPending: false, unlockHistoryPromptVisible: true, lockedHistoryCount: countLockedHistoryMessages(context.messages), unlockHistoryError: null, - })), + }), +); - dismissUnlockHistoryPrompt: chatAssign(() => ({ - paymentUnlockPending: false, - unlockHistoryPromptVisible: false, - unlockHistoryError: null, - })), +export const dismissUnlockHistoryPromptAction = chatAssign(() => ({ + paymentUnlockPending: false, + unlockHistoryPromptVisible: false, + unlockHistoryError: null, +})); - markUnlockHistoryStarted: chatAssign(() => ({ - paymentUnlockPending: false, - unlockHistoryPromptVisible: false, - isUnlockingHistory: true, - unlockHistoryError: null, - })), +export const markUnlockHistoryStartedAction = chatAssign(() => ({ + paymentUnlockPending: false, + unlockHistoryPromptVisible: false, + isUnlockingHistory: true, + unlockHistoryError: null, +})); - markUnlockHistoryFailed: chatAssign(() => ({ - isUnlockingHistory: false, - unlockHistoryPromptVisible: true, - unlockHistoryError: "Failed to unlock messages. Please try again.", - })), +export const markUnlockHistoryFailedAction = chatAssign(() => ({ + isUnlockingHistory: false, + unlockHistoryPromptVisible: true, + unlockHistoryError: "Failed to unlock messages. Please try again.", +})); - markUnlockMessageStarted: chatAssign(({ event }: ChatActionArgs) => { +export const markUnlockMessageStartedAction = chatAssign( + ({ event }: ChatActionArgs) => { if (event.type !== "ChatUnlockMessageRequested") return {}; return { isUnlockingMessage: true, @@ -122,9 +123,11 @@ export const chatUnlockActions = { unlockMessageError: null, unlockPaywallRequest: null, }; - }), + }, +); - applyUnlockMessageSucceeded: chatAssign(({ context, event }: ChatActionArgs) => { +export const applyUnlockMessageSucceededAction = chatAssign( + ({ context, event }: ChatActionArgs) => { const output = (event as { output?: UnlockMessageOutput }).output; if (!output) return {}; return { @@ -139,9 +142,11 @@ export const chatUnlockActions = { requiredCredits: 0, shortfallCredits: 0, }; - }), + }, +); - requestUnlockPaymentFromOutput: chatAssign(({ context, event }: ChatActionArgs) => { +export const requestUnlockPaymentFromOutputAction = chatAssign( + ({ context, event }: ChatActionArgs) => { const output = (event as { output?: UnlockMessageOutput }).output; if (!output) return {}; return { @@ -161,9 +166,11 @@ export const chatUnlockActions = { requiredCredits: output.response.requiredCredits, shortfallCredits: output.response.shortfallCredits, }; - }), + }, +); - requestUnlockPaymentFromError: chatAssign(({ context }: ChatContextArgs) => ({ +export const requestUnlockPaymentFromErrorAction = chatAssign( + ({ context }: ChatContextArgs) => ({ isUnlockingMessage: false, unlockMessageError: "unlock_failed", unlockPaywallRequest: @@ -179,13 +186,10 @@ export const chatUnlockActions = { : null, unlockingMessageId: null, unlockingMessageKind: null, - })), + }), +); - clearUnlockPaywallRequest: chatAssign(() => ({ - unlockPaywallRequest: null, - })), -}; - -export const unlockHistoryActor = chatUnlockActors.unlockHistory; -export const unlockMessageActor = chatUnlockActors.unlockMessage; +export const clearUnlockPaywallRequestAction = chatAssign(() => ({ + unlockPaywallRequest: null, +})); export type { UnlockMessageOutput } from "./chat-machine.helpers";