chore(chat): refactor chat init into separate quota and history actors
Split `chatInitActor` into `loadQuotaActor` (guest quota fetch) and `loadHistoryActor` (local → network → save sync), and add `quotaLoaded` / `historyLoaded` state flags so the UI can display loading status during the new local-first history hydration flow. Also drop the now-unused `ChatInit` event and pipe Next.js stdout/stderr to a persistent log file in the post-receive hook for easier troubleshooting.
This commit is contained in:
@@ -6,34 +6,42 @@
|
||||
* - 仓库注入(接口类型 → 单例)
|
||||
* - DTO ↔ UiMessage 映射(**纯函数** —— 可单测)
|
||||
* - Result → number 映射(**纯函数** —— 可单测)
|
||||
* - 初始数据装配(**仅**游客有意义 —— `chatInitActor` 调用)
|
||||
* - 2 **个**独立 init 任务**的**数据加载**:
|
||||
* - `readGuestQuota()` —— 游客**日**配 + 总**配**(**仅**游客有意义,**纯** ChatStorage)
|
||||
* - `readAndSyncHistory()` —— local → network → save network to local 3 **步**
|
||||
*
|
||||
* 设计目标:
|
||||
* - helpers **无 XState 依赖**(可独立 import / 测试)
|
||||
* - actors 依赖 helpers(import)
|
||||
* - machine 依赖 actors(import)—— **不**直接依赖 helpers
|
||||
*
|
||||
* 历史:
|
||||
* - `readInitData` + `InitResult` 已**删**(**被** 2 **个**独立 helper **替**换**)
|
||||
* - `AuthStorage` import **已**删**(**只** `readInitData` 用过,**移**到** `chatInit` actor **上**层**调**用**)
|
||||
*/
|
||||
|
||||
import type { UiMessage } from "@/models/chat/ui-message";
|
||||
import { chatRepository } from "@/data/repositories/chat_repository";
|
||||
import type { IChatRepository } from "@/data/repositories/interfaces";
|
||||
import { ChatStorage } from "@/data/storage/chat/chat_storage";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
// ============================================================// Constants
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
/** 翻历史每页大小(**仅** `loadMoreHistoryActor` 用) */
|
||||
export const PAGE_SIZE = 20;
|
||||
|
||||
// ============================================================// Repository injection
|
||||
// ============================================================
|
||||
// Repository injection
|
||||
// ============================================================
|
||||
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
|
||||
export const chatRepo: IChatRepository = chatRepository;
|
||||
|
||||
// ============================================================// DTO ↔ UiMessage 映射
|
||||
// ============================================================
|
||||
// DTO ↔ UiMessage 映射
|
||||
// ============================================================
|
||||
/**
|
||||
* ChatMessage[] → UiMessage[](**纯函数**)
|
||||
@@ -67,7 +75,8 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================// Result → number 映射
|
||||
// ============================================================
|
||||
// Result → number 映射
|
||||
// ============================================================
|
||||
/** 日配额 Result → number(**纯函数**) */
|
||||
export function mapQuotaResult(
|
||||
@@ -87,45 +96,100 @@ export function mapTotalQuotaResult(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// ============================================================// Initial data
|
||||
// ============================================================
|
||||
export interface InitResult {
|
||||
localMessages: UiMessage[];
|
||||
isGuest: boolean;
|
||||
guestRemainingQuota: number;
|
||||
guestTotalQuota: number;
|
||||
}
|
||||
|
||||
// Init 任务 1:游客配额(**纯** local)—— `loadQuotaActor` 调
|
||||
// ============================================================
|
||||
/**
|
||||
* 读取初始数据(`chatInitActor` 调用)
|
||||
*
|
||||
* - `isGuest` 在 chat 业务层仅用于"是否加载本地历史" + "onDone 条件赋配额"
|
||||
* —— **不**是鉴权信号。鉴权判断已上提到 chat-screen 派生。
|
||||
* - 配额 fallback 0 —— 非游客路径用不到;onDone 会**条件**赋。
|
||||
* 读取游客配额(**仅** guestSession 有意义)
|
||||
* - 游客**日**配 + 总**配** 两**项**都**从** ChatStorage 拉(**纯** local,**无**网络)
|
||||
* - 失**败**返 fallback 0(**不**抛)—— **让** UI **还**是**能**进** ready
|
||||
*/
|
||||
export async function readInitData(): Promise<InitResult> {
|
||||
export async function readGuestQuota(): Promise<{
|
||||
remaining: number;
|
||||
total: number;
|
||||
}> {
|
||||
const chatStorage = ChatStorage.getInstance();
|
||||
const isGuest = !AuthStorage.getInstance().hasLoginToken();
|
||||
|
||||
const [dailyResult, totalResult, localResult] = await Promise.all([
|
||||
const [dailyResult, totalResult] = await Promise.all([
|
||||
chatStorage.getGuestDailyChatQuota(),
|
||||
chatStorage.getGuestTotalQuota(),
|
||||
isGuest ? chatRepo.getLocalMessages() : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
return {
|
||||
isGuest,
|
||||
guestRemainingQuota: mapQuotaResult(
|
||||
remaining: mapQuotaResult(
|
||||
dailyResult as Result<{ remaining: number } | null>,
|
||||
0,
|
||||
),
|
||||
guestTotalQuota: mapTotalQuotaResult(
|
||||
totalResult as Result<number | null>,
|
||||
0,
|
||||
),
|
||||
localMessages:
|
||||
localResult && Result.isOk(localResult) && localResult.data
|
||||
? localMessagesToUi(localResult.data)
|
||||
: [],
|
||||
total: mapTotalQuotaResult(totalResult as Result<number | null>, 0),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Init 任务 2:history 3 步流(local → network → save)—— `loadHistoryActor` 调
|
||||
// ============================================================
|
||||
/**
|
||||
* 拉 history **流**程(**每**步**都**记**日**志** —— 用户要求"**不**要**删**除**")
|
||||
* 1. **读** local(chatRepo.getLocalMessages)→ **返** localMessages
|
||||
* 2. **读** network(chatRepo.getHistory)→ **返** networkMessages
|
||||
* 3. **用** network **覆**盖** local(chatRepo.saveMessagesToLocal)→ local **被**同步
|
||||
*
|
||||
* **返**回** network **最**终** messages**(authorititative),**不**返** local(UI **不**需**要**过渡**态**)
|
||||
*
|
||||
* **保**留**日**志**(**前**缀 `[chat-machine]` 与 chat-machine.actors.ts **对**齐**)
|
||||
*/
|
||||
export async function readAndSyncHistory(): Promise<{
|
||||
messages: UiMessage[]; // network **最**终** messages(authorititative)
|
||||
hasMore: boolean;
|
||||
newOffset: number;
|
||||
localOverwritten: boolean; // true = local **已**被 network **覆**盖**(**写**入** ChatStorage)
|
||||
/** 真实从 local 读到的消息数(**调**试**用**) */
|
||||
localCount: number;
|
||||
/** 真实从 network 读到的消息数(**调**试**用**) */
|
||||
networkCount: number;
|
||||
}> {
|
||||
// 1. **读** local
|
||||
const localResult = await chatRepo.getLocalMessages();
|
||||
const localMessages =
|
||||
Result.isOk(localResult) && localResult.data
|
||||
? localMessagesToUi(localResult.data)
|
||||
: [];
|
||||
console.log("[chat-machine] loadHistory LOCAL DONE", {
|
||||
count: localMessages.length,
|
||||
});
|
||||
|
||||
// 2. **读** network
|
||||
const networkResult = await chatRepo.getHistory(PAGE_SIZE, 0);
|
||||
if (!Result.isOk(networkResult)) {
|
||||
// network 失**败** —— **返**空 messages(**不**让 UI 卡**住**)
|
||||
console.error(
|
||||
"[chat-machine] loadHistory NETWORK FAILED",
|
||||
networkResult.success ? null : (networkResult as { error: unknown }).error,
|
||||
);
|
||||
return {
|
||||
messages: localMessages, // **退**而**求**其**次**:返 local(**不**然**屏**幕**空**)
|
||||
hasMore: false,
|
||||
newOffset: 0,
|
||||
localOverwritten: false,
|
||||
localCount: localMessages.length,
|
||||
networkCount: 0,
|
||||
};
|
||||
}
|
||||
const networkUi = localMessagesToUi(networkResult.data.messages);
|
||||
console.log("[chat-machine] loadHistory NETWORK DONE", {
|
||||
count: networkUi.length,
|
||||
});
|
||||
|
||||
// 3. **用** network **覆**盖** local("**再**用**网**络**数**据**覆**盖**本**地**数**据"**这**句**的**字**面**实**现**)
|
||||
const saveResult = await chatRepo.saveMessagesToLocal(networkResult.data.messages);
|
||||
const localOverwritten = Result.isOk(saveResult);
|
||||
console.log("[chat-machine] loadHistory SAVE TO LOCAL DONE", {
|
||||
localOverwritten,
|
||||
});
|
||||
|
||||
return {
|
||||
messages: networkUi, // authorititative
|
||||
hasMore: networkUi.length >= PAGE_SIZE,
|
||||
newOffset: networkUi.length,
|
||||
localOverwritten,
|
||||
localCount: localMessages.length,
|
||||
networkCount: networkUi.length,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user