Files
cozsweet-frontend-nextjs/src/stores/user/user-machine.helpers.ts
T
admin a44463b3ee 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.
2026-06-16 18:12:11 +08:00

105 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* User 状态机:纯函数 + 数据加载
*
* 从 `user-machine.ts` 抽出的"Helpers"段:
* - InitData 形状
* - `toView`DTO → UserView 映射(纯函数)
* - `readInitData`:从 UserStorage 读 user + avatarUrl(并行)
* - `userStorage` 单例:被 helpers 和 actors 共用
*
* 设计目标:
* - helpers 无 XState 依赖(可独立 import / 测试)
* - actors 依赖 helpersimport
* - machine 依赖 actorsimport)—— 不直接依赖 helpers
*/
import type { UserView } from "@/models/user/user";
import { UserStorage } from "@/data/storage/user/user_storage";
import { Result } from "@/utils/result";
// ============================================================
// Storage singleton
// ============================================================
/**
* UserStorage 单例 —— 被 helpersreadInitData)和 actorsuserFetchActor / 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 + avatarUrlUserStorage 两个独立接口)。
* - 任意一个失败 / 无数据 → 对应字段返回 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 };
}