fix(chat): isolate pending flows and cancel stale requests

This commit is contained in:
2026-07-17 19:22:01 +08:00
parent 2fc312b5c7
commit 8bb1e21886
27 changed files with 501 additions and 92 deletions
@@ -20,6 +20,7 @@ const defaultScope = {
};
const promotionSession: PendingChatPromotion = {
characterId: "character_elio",
promotionType: "image",
lockType: "image_paywall",
clientLockId: "promotion-1",
@@ -181,6 +182,7 @@ function createPendingUnlock(
): PendingChatUnlock {
return {
reason: "single_message_unlock",
characterId: "character_elio",
...input,
stage: "auth",
createdAt: 1,
+3 -1
View File
@@ -58,7 +58,9 @@ export function ChatScreen() {
loginStatus: authState.loginStatus,
messages: state.historyMessages,
});
const isPromotionBootstrapReady = useChatPromotionBootstrap();
const isPromotionBootstrapReady = useChatPromotionBootstrap(
state.characterId,
);
const visibleMessages = isPromotionBootstrapReady
? state.messages
: state.historyMessages;
@@ -11,7 +11,7 @@ import { Logger } from "@/utils/logger";
const log = new Logger("UseChatPromotionBootstrap");
export function useChatPromotionBootstrap(): boolean {
export function useChatPromotionBootstrap(characterId: string): boolean {
const chatDispatch = useChatDispatch();
const startedRef = useRef(false);
const [isReady, setIsReady] = useState(false);
@@ -23,8 +23,8 @@ export function useChatPromotionBootstrap(): boolean {
void (async () => {
try {
const [entryPromotion, pendingUnlock] = await Promise.all([
consumePendingChatPromotion(),
peekPendingChatUnlock(),
consumePendingChatPromotion(characterId),
peekPendingChatUnlock(characterId),
]);
const promotion = entryPromotion ?? pendingUnlock?.promotion ?? null;
@@ -46,7 +46,7 @@ export function useChatPromotionBootstrap(): boolean {
setIsReady(true);
}
})();
}, [chatDispatch]);
}, [characterId, chatDispatch]);
return isReady;
}
@@ -79,6 +79,7 @@ export function useChatUnlockCoordinator({
const trackedPaywallRef = useRef<string | null>(null);
const chatState = useChatSelector(
(state) => ({
characterId: state.context.characterId,
historyLoaded: state.context.historyLoaded,
isUnlockingHistory: state.matches({ userSession: "unlockingHistory" }),
lockedHistoryCount: state.context.lockedHistoryCount,
@@ -125,7 +126,7 @@ export function useChatUnlockCoordinator({
let cancelled = false;
const resumePendingUnlock = async () => {
const pending = await peekPendingChatUnlock();
const pending = await peekPendingChatUnlock(chatState.characterId);
if (
cancelled ||
!pending ||
@@ -138,7 +139,7 @@ export function useChatUnlockCoordinator({
return;
}
const consumed = await consumePendingChatUnlock();
const consumed = await consumePendingChatUnlock(chatState.characterId);
if (cancelled || !consumed) return;
chatDispatch({
@@ -157,6 +158,7 @@ export function useChatUnlockCoordinator({
};
}, [
chatDispatch,
chatState.characterId,
chatState.historyLoaded,
defaultReturnUrl,
enabled,
@@ -15,6 +15,10 @@ import {
savePendingChatPromotion,
} from "@/lib/navigation/chat_unlock_session";
import { ROUTES } from "@/router/routes";
import {
DEFAULT_CHARACTER_ID,
getCharacterBySlug,
} from "@/data/constants/character";
import { Logger } from "@/utils/logger";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
@@ -49,6 +53,8 @@ export default function ExternalEntryPersist({
const hasReinitializedForPsidRef = useRef(false);
const targetRoute = resolveExternalEntryTarget({ target });
const destination = resolveExternalEntryDestination({ target, character });
const characterId =
getCharacterBySlug(character)?.id ?? DEFAULT_CHARACTER_ID;
const resolvedPromotionType = resolveExternalEntryPromotionType({
mode,
promotionType,
@@ -66,7 +72,7 @@ export default function ExternalEntryPersist({
avatarUrl,
}),
targetRoute === ROUTES.chat && resolvedPromotionType
? savePendingChatPromotion(resolvedPromotionType)
? savePendingChatPromotion(resolvedPromotionType, characterId)
: clearPendingChatPromotion(),
]);
for (const result of results) {
@@ -87,6 +93,7 @@ export default function ExternalEntryPersist({
avatarUrl,
asid,
character,
characterId,
destination,
deviceId,
mode,