refactor(user): split user-machine into helpers and actors
Mirror the existing chat-module structure (chat-machine.helpers.ts + chat-machine.actors.ts) for consistency across state-machine modules. Split: - user-machine.helpers.ts: pure data layer (no XState dep) - userStorage singleton - InitData interface - toView DTO → UserView mapper - readInitData (parallel getUser + getAvatarUrl) - user-machine.actors.ts: async actor implementations (fromPromise) - userRepo / authRepo injection - userInitActor / userFetchActor / userLogoutActor - user-machine.ts: only setup().createMachine() — keeps inline assign actions referencing context/event where they belong, imports actors (not helpers, per the layered convention). No behaviour change — only file boundaries moved. Public API (exports of UserState / UserEvent / initialState / userMachine / UserMachine) stays identical so external imports are untouched.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* User 状态机:纯函数 + 数据加载
|
||||
*
|
||||
* 从 `user-machine.ts` 抽出的"Helpers"段:
|
||||
* - InitData 形状
|
||||
* - `toView`:DTO → UserView 映射(纯函数)
|
||||
* - `readInitData`:从 UserStorage 读 user + avatarUrl(并行)
|
||||
* - `userStorage` 单例:被 helpers 和 actors 共用
|
||||
*
|
||||
* 设计目标:
|
||||
* - helpers 无 XState 依赖(可独立 import / 测试)
|
||||
* - actors 依赖 helpers(import)
|
||||
* - machine 依赖 actors(import)—— 不直接依赖 helpers
|
||||
*/
|
||||
|
||||
import type { UserView } from "@/models/user/user";
|
||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
// ============================================================
|
||||
// Storage singleton
|
||||
// ============================================================
|
||||
/**
|
||||
* UserStorage 单例 —— 被 helpers(readInitData)和 actors(userFetchActor / userLogoutActor)
|
||||
* 共同使用。从 helpers 导出,避免在 actors 里重复 `UserStorage.getInstance()`。
|
||||
*/
|
||||
export const userStorage = UserStorage.getInstance();
|
||||
|
||||
// ============================================================
|
||||
// InitData 形状
|
||||
// ============================================================
|
||||
/**
|
||||
* `userInitActor` 的输出形状(user-machine.initializing.onDone 用)——
|
||||
* 拆分 user 和 avatarUrl 是因为它们来自 UserStorage 两个独立接口。
|
||||
*/
|
||||
export interface InitData {
|
||||
user: UserView | null;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DTO → UserView 映射
|
||||
// ============================================================
|
||||
/**
|
||||
* UserView DTO → UserView 视图模型(纯函数,可单测)
|
||||
* - 与 chat 的 `localMessagesToUi` 同模式
|
||||
* - 默认值收敛在此:undefined → "" / 0 / false,避免下游到处判空
|
||||
*/
|
||||
export function toView(u: {
|
||||
id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
intimacy?: number;
|
||||
dolBalance?: number;
|
||||
relationshipStage?: string;
|
||||
currentMood?: string;
|
||||
isGuest?: boolean;
|
||||
isVip?: boolean;
|
||||
voiceMinutesRemaining?: number;
|
||||
stripeCustomerId?: string | null;
|
||||
}): UserView {
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email ?? "",
|
||||
avatarUrl: u.avatarUrl ?? "",
|
||||
intimacy: u.intimacy ?? 0,
|
||||
dolBalance: u.dolBalance ?? 0,
|
||||
relationshipStage: u.relationshipStage ?? "",
|
||||
currentMood: u.currentMood ?? "",
|
||||
isGuest: u.isGuest ?? false,
|
||||
isVip: u.isVip ?? false,
|
||||
voiceMinutesRemaining: u.voiceMinutesRemaining ?? 0,
|
||||
stripeCustomerId: u.stripeCustomerId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Init 数据加载(userInitActor 用)
|
||||
// ============================================================
|
||||
/**
|
||||
* 并行读 user + avatarUrl(UserStorage 两个独立接口)。
|
||||
* - 任意一个失败 / 无数据 → 对应字段返回 null,不抛错(与 chat 的
|
||||
* `readAndSyncHistory` "失败不卡 init" 一致的设计)
|
||||
* - Result.isOk(_) && data 双重判空:Result 包装层 + 业务 null
|
||||
*/
|
||||
export async function readInitData(): Promise<InitData> {
|
||||
const [userResult, avatarResult] = await Promise.all([
|
||||
userStorage.getUser(),
|
||||
userStorage.getAvatarUrl(),
|
||||
]);
|
||||
|
||||
let user: UserView | null = null;
|
||||
if (Result.isOk(userResult) && userResult.data) {
|
||||
user = toView(userResult.data);
|
||||
}
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
if (Result.isOk(avatarResult) && avatarResult.data && avatarResult.data.length > 0) {
|
||||
avatarUrl = avatarResult.data;
|
||||
}
|
||||
|
||||
return { user, avatarUrl };
|
||||
}
|
||||
Reference in New Issue
Block a user