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,74 @@
|
|||||||
|
/**
|
||||||
|
* User 状态机:Actors(异步服务)
|
||||||
|
*
|
||||||
|
* 从 `user-machine.ts` 抽出的"Actors"段:
|
||||||
|
* - `userInitActor`(fromPromise:读 UserStorage 并行 user + avatarUrl)
|
||||||
|
* - `userFetchActor`(fromPromise:调 API + 持久化到本地)
|
||||||
|
* - `userLogoutActor`(fromPromise:auth logout + 清本地数据)
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 依赖 `user-machine.helpers.ts`(toView, readInitData, userStorage)
|
||||||
|
* - 仓库以接口类型注入(调用面只看接口,运行时仍是同一单例)
|
||||||
|
* - 命名约定:`[user-machine]` 前缀日志(如未来加)—— 与 chat 模块对齐
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fromPromise } from "xstate";
|
||||||
|
|
||||||
|
import type { UserView } from "@/models/user/user";
|
||||||
|
import { userRepository } from "@/data/repositories/user_repository";
|
||||||
|
import { authRepository } from "@/data/repositories/auth_repository";
|
||||||
|
import type {
|
||||||
|
IAuthRepository,
|
||||||
|
IUserRepository,
|
||||||
|
} from "@/data/repositories/interfaces";
|
||||||
|
import { Result } from "@/utils/result";
|
||||||
|
|
||||||
|
import { type InitData, readInitData, toView, userStorage } from "./user-machine.helpers";
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 仓库注入
|
||||||
|
// ============================================================
|
||||||
|
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
|
||||||
|
const userRepo: IUserRepository = userRepository;
|
||||||
|
const authRepo: IAuthRepository = authRepository;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Init actor:从 UserStorage 读 user + avatarUrl
|
||||||
|
// ============================================================
|
||||||
|
/**
|
||||||
|
* 读本地 user + avatarUrl(并行)。
|
||||||
|
* - 失败/无数据 → 返回 null,不抛错(machine 走 onDone 回到 idle,context 是 null)
|
||||||
|
* - onDone 赋 currentUser + avatarUrl 到 context
|
||||||
|
*/
|
||||||
|
export const userInitActor = fromPromise<InitData>(async () => readInitData());
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Fetch actor:调 API 拉当前用户 + 持久化
|
||||||
|
// ============================================================
|
||||||
|
/**
|
||||||
|
* 调 `userRepo.getCurrentUser` 拉当前 user:
|
||||||
|
* - 成功 → 转 UserView + 写本地(userStorage.setUser)→ 返回 View
|
||||||
|
* - 失败 → 返回 null(machine 走 onDone,context 保持不变;onError 兜底)
|
||||||
|
*/
|
||||||
|
export const userFetchActor = fromPromise<UserView | null>(async () => {
|
||||||
|
const result = await userRepo.getCurrentUser();
|
||||||
|
if (Result.isErr(result)) return null;
|
||||||
|
const view = toView(result.data.toJson());
|
||||||
|
// 持久化到本地
|
||||||
|
await userStorage.setUser(result.data.toJson());
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Logout actor:后端登出 + 清本地用户数据
|
||||||
|
// ============================================================
|
||||||
|
/**
|
||||||
|
* 登出:
|
||||||
|
* - 调 `authRepo.logout` 让后端吊销 token
|
||||||
|
* - `userStorage.clearUserData` 清本地 user / avatar 等
|
||||||
|
* - onDone / onError 都跑 `clearUser` action(machine 层)—— 失败也清本地
|
||||||
|
*/
|
||||||
|
export const userLogoutActor = fromPromise<void>(async () => {
|
||||||
|
await authRepo.logout();
|
||||||
|
await userStorage.clearUserData();
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -5,112 +5,31 @@
|
|||||||
*
|
*
|
||||||
* 设计要点:
|
* 设计要点:
|
||||||
* - XState v5 `setup({...}).createMachine({...})` 声明式 API
|
* - XState v5 `setup({...}).createMachine({...})` 声明式 API
|
||||||
* - HTTP 操作用 `fromPromise` actor(一次性 Promise)
|
* - HTTP 操作用 `fromPromise` actor(一次性 Promise)→ 见 `user-machine.actors.ts`
|
||||||
|
* - 纯函数 / 数据加载 → 见 `user-machine.helpers.ts`
|
||||||
* - 同步状态更新用 `assign` actions(inline 在 setup() 中)
|
* - 同步状态更新用 `assign` actions(inline 在 setup() 中)
|
||||||
* - 保持事件类型名与原 Dart 一致
|
* - 保持事件类型名与原 Dart 一致
|
||||||
|
*
|
||||||
|
* 分层:
|
||||||
|
* - helpers: 无 XState 依赖(DTO 映射 + 数据加载)
|
||||||
|
* - actors: 依赖 helpers(异步 actor 实现)
|
||||||
|
* - machine: 依赖 actors(XState setup + 状态图)
|
||||||
*/
|
*/
|
||||||
import { setup, fromPromise, assign } from "xstate";
|
import { setup, assign } from "xstate";
|
||||||
|
|
||||||
import type { UserView } from "@/models/user/user";
|
|
||||||
import { userRepository } from "@/data/repositories/user_repository";
|
|
||||||
import { authRepository } from "@/data/repositories/auth_repository";
|
|
||||||
import type {
|
|
||||||
IAuthRepository,
|
|
||||||
IUserRepository,
|
|
||||||
} from "@/data/repositories/interfaces";
|
|
||||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
|
||||||
import { Result } from "@/utils/result";
|
|
||||||
|
|
||||||
import { UserState, initialState } from "./user-state";
|
import { UserState, initialState } from "./user-state";
|
||||||
import type { UserEvent } from "./user-events";
|
import type { UserEvent } from "./user-events";
|
||||||
|
import {
|
||||||
|
userInitActor,
|
||||||
|
userFetchActor,
|
||||||
|
userLogoutActor,
|
||||||
|
} from "./user-machine.actors";
|
||||||
|
|
||||||
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
|
// 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变
|
||||||
export type { UserState } from "./user-state";
|
export type { UserState } from "./user-state";
|
||||||
export { initialState } from "./user-state";
|
export { initialState } from "./user-state";
|
||||||
export type { UserEvent } from "./user-events";
|
export type { UserEvent } from "./user-events";
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Helpers
|
|
||||||
// ============================================================
|
|
||||||
const userStorage = UserStorage.getInstance();
|
|
||||||
|
|
||||||
interface InitData {
|
|
||||||
user: UserView | null;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Actors
|
|
||||||
// ============================================================
|
|
||||||
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
|
|
||||||
const userRepo: IUserRepository = userRepository;
|
|
||||||
const authRepo: IAuthRepository = authRepository;
|
|
||||||
|
|
||||||
const userInitActor = fromPromise<InitData>(async () => readInitData());
|
|
||||||
|
|
||||||
const userFetchActor = fromPromise<UserView | null>(async () => {
|
|
||||||
const result = await userRepo.getCurrentUser();
|
|
||||||
if (Result.isErr(result)) return null;
|
|
||||||
const view = toView(result.data.toJson());
|
|
||||||
// 持久化到本地
|
|
||||||
await userStorage.setUser(result.data.toJson());
|
|
||||||
return view;
|
|
||||||
});
|
|
||||||
|
|
||||||
const userLogoutActor = fromPromise<void>(async () => {
|
|
||||||
await authRepo.logout();
|
|
||||||
await userStorage.clearUserData();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Machine
|
// Machine
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user