refactor(chat): split machine by business flow

This commit is contained in:
2026-07-14 19:04:32 +08:00
parent e48efc36ba
commit 84e677f687
11 changed files with 1403 additions and 1295 deletions
+253
View File
@@ -0,0 +1,253 @@
import { type DoneActorEvent } from "xstate";
import {
applyPromotionUnlockOutput,
applySingleUnlockOutput,
countLockedHistoryMessages,
type UnlockMessageOutput,
} from "../chat-machine.helpers";
import type { UnlockHistoryOutput } from "../chat-unlock-flow";
import { sendMachineSetup } from "./send-flow";
import { createChatActorActionSetup } from "./setup";
const markPaymentUnlockPendingAction = sendMachineSetup.assign(() => ({
paymentUnlockPending: true,
unlockHistoryError: null,
}));
const showUnlockHistoryPromptAction = sendMachineSetup.assign(
({ context }) => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: true,
lockedHistoryCount: countLockedHistoryMessages(context.messages),
unlockHistoryError: null,
}),
);
const dismissUnlockHistoryPromptAction = sendMachineSetup.assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
unlockHistoryError: null,
}));
const markUnlockHistoryStartedAction = sendMachineSetup.assign(() => ({
paymentUnlockPending: false,
unlockHistoryPromptVisible: false,
isUnlockingHistory: true,
unlockHistoryError: null,
}));
const markUnlockHistoryFailedAction = sendMachineSetup.assign(() => ({
isUnlockingHistory: false,
unlockHistoryPromptVisible: true,
unlockHistoryError: "Failed to unlock messages. Please try again.",
}));
const markUnlockMessageStartedAction = sendMachineSetup.assign(
({ event }) => {
if (event.type !== "ChatUnlockMessageRequested") return {};
return {
isUnlockingMessage: true,
unlockingMessageId: event.messageId,
unlockingMessageKind: event.kind,
unlockingRemoteMessageId:
event.remoteMessageId ?? (event.lockType ? null : event.messageId),
unlockingLockType: event.lockType ?? null,
unlockingClientLockId: event.clientLockId ?? null,
unlockMessageError: null,
unlockPaywallRequest: null,
};
},
);
const applyUnlockMessageSucceededAction = sendMachineSetup.assign(
({ context, event }) => {
const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>;
return {
messages: applySingleUnlockOutput(context.messages, output),
promotion: applyPromotionUnlockOutput(context.promotion, output),
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockingRemoteMessageId: null,
unlockingLockType: null,
unlockingClientLockId: null,
unlockMessageError: null,
unlockPaywallRequest: null,
creditBalance: output.response.creditBalance,
creditsCharged: output.response.creditsCharged,
requiredCredits: 0,
shortfallCredits: 0,
};
},
);
const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign(
({ context, event }) => {
const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>;
return {
promotion: applyPromotionUnlockOutput(context.promotion, output),
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockingRemoteMessageId: null,
unlockingLockType: null,
unlockingClientLockId: null,
unlockMessageError: output.response.reason,
unlockPaywallRequest: {
displayMessageId:
output.response.messageId || output.displayMessageId,
...(output.response.messageId || output.request.messageId
? {
messageId:
output.response.messageId || output.request.messageId,
}
: {}),
kind: context.unlockingMessageKind ?? "private",
...(output.request.lockType
? { lockType: output.request.lockType }
: {}),
...(output.request.clientLockId
? { clientLockId: output.request.clientLockId }
: {}),
...(context.promotion?.message.id === output.displayMessageId
? { promotion: context.promotion.session }
: {}),
reason: output.response.reason,
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
},
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
};
},
);
const requestUnlockPaymentFromErrorAction = sendMachineSetup.assign(
({ context }) => ({
isUnlockingMessage: false,
unlockMessageError: "unlock_failed",
unlockPaywallRequest:
context.unlockingMessageId && context.unlockingMessageKind
? {
displayMessageId: context.unlockingMessageId,
...(context.unlockingRemoteMessageId
? { messageId: context.unlockingRemoteMessageId }
: {}),
kind: context.unlockingMessageKind,
...(context.unlockingLockType
? { lockType: context.unlockingLockType }
: {}),
...(context.unlockingClientLockId
? { clientLockId: context.unlockingClientLockId }
: {}),
...(context.promotion?.message.id === context.unlockingMessageId
? { promotion: context.promotion.session }
: {}),
reason: "unlock_failed",
creditBalance: context.creditBalance,
requiredCredits: context.requiredCredits,
shortfallCredits: context.shortfallCredits,
}
: null,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockingRemoteMessageId: null,
unlockingLockType: null,
unlockingClientLockId: null,
}),
);
const clearUnlockPaywallRequestAction = sendMachineSetup.assign(() => ({
unlockPaywallRequest: null,
}));
export const unlockMachineSetup = sendMachineSetup.extend({
actions: {
markPaymentUnlockPending: markPaymentUnlockPendingAction,
showUnlockHistoryPrompt: showUnlockHistoryPromptAction,
dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction,
markUnlockHistoryStarted: markUnlockHistoryStartedAction,
markUnlockHistoryFailed: markUnlockHistoryFailedAction,
markUnlockMessageStarted: markUnlockMessageStartedAction,
applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction,
requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction,
requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction,
clearUnlockPaywallRequest: clearUnlockPaywallRequestAction,
},
});
const unlockHistoryDoneActionSetup =
createChatActorActionSetup<DoneActorEvent<UnlockHistoryOutput>>();
const applyUnlockHistoryOutputAction =
unlockHistoryDoneActionSetup.assign(({ event }) => {
const lockedHistoryCount = countLockedHistoryMessages(
event.output.messages,
);
const insufficientBalance =
!event.output.unlocked &&
event.output.reason === "insufficient_balance";
return {
messages: event.output.messages,
historyLoaded: true,
paymentUnlockPending: false,
unlockHistoryPromptVisible: insufficientBalance,
lockedHistoryCount,
isUnlockingHistory: false,
unlockHistoryError: insufficientBalance
? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.`
: null,
};
});
export const unlockingHistoryState = unlockMachineSetup.createStateConfig({
invoke: {
id: "unlockHistory",
src: "unlockHistory",
onDone: {
target: "ready",
actions: applyUnlockHistoryOutputAction,
},
onError: {
target: "ready",
actions: "markUnlockHistoryFailed",
},
},
});
export const unlockingMessageState = unlockMachineSetup.createStateConfig({
invoke: {
id: "unlockMessage",
src: "unlockMessage",
input: ({ context }) => ({
displayMessageId: context.unlockingMessageId ?? "",
...(context.unlockingRemoteMessageId
? { messageId: context.unlockingRemoteMessageId }
: {}),
...(context.unlockingLockType
? { lockType: context.unlockingLockType }
: {}),
...(context.unlockingClientLockId
? { clientLockId: context.unlockingClientLockId }
: {}),
}),
onDone: [
{
guard: ({ event }) => event.output.response.unlocked,
target: "ready",
actions: "applyUnlockMessageSucceeded",
},
{
target: "ready",
actions: "requestUnlockPaymentFromOutput",
},
],
onError: {
target: "ready",
actions: "requestUnlockPaymentFromError",
},
},
});