feat: add xstate state management for user state

Introduce xstate v5 and @xstate/react to manage user authentication and profile state. Implement userMachine with actors for initialization, fetching, and logout operations, along with actions for updating user data and clearing state.
This commit is contained in:
2026-06-09 17:26:16 +08:00
parent 50940961ec
commit 5e645b003e
33 changed files with 1466 additions and 1273 deletions
+123
View File
@@ -0,0 +1,123 @@
"use client";
/**
* localStorage 通用包装
*
* 设计要点:
* 1. 全部方法返回 `Promise<Result<T>>`,与 `LocalChatStorage`Dexie/异步)保持签名一致,
* 调用方可以统一 `await` 而不必关心底层是同步还是异步。
* 2. 三重 SSR 保护:
* - 文件顶 `"use client"` 指令
* - 构造时探测 `typeof window`,服务端拿不到 `window.localStorage` 时存 `null`
* - 每次调用入口再做可用性检查,避免运行时炸
* 3. 测试友好:构造函数接受可选 `Storage` 注入,单例可通过 `_resetForTests` 重置。
* 4. `getString` 把空串视作 `null`(对齐 Dart `SpAsyncUtil.getString` 行为)。
* 5. `getJson` / `setJson` 在边界做 Zod 校验,读写都走 schema。
*/
import { type ZodType } from "zod";
import { Result, type Result as ResultT } from "@/utils/result";
const SSR_ERROR_MSG =
"localStorage is not available in this environment (SSR or non-browser)";
export class LocalStorage {
private readonly storage: Storage | null;
private static _instance: LocalStorage | null = null;
constructor(storage?: Storage | null) {
this.storage =
storage !== undefined
? storage
: typeof window !== "undefined"
? window.localStorage
: null;
}
/** 全局单例(仅在浏览器环境有效)。 */
static getInstance(): LocalStorage {
if (!LocalStorage._instance) {
LocalStorage._instance = new LocalStorage();
}
return LocalStorage._instance;
}
/** @internal 测试用:重置单例以便每个测试用例拿到独立实例。 */
static _resetForTests(): void {
LocalStorage._instance = null;
}
private isAvailable(): boolean {
return this.storage !== null;
}
async getString(key: string): Promise<ResultT<string | null>> {
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
try {
const v = this.storage!.getItem(key);
if (v === null || v === "") return Result.ok(null);
return Result.ok(v);
} catch (e) {
return Result.err(e);
}
}
async setString(key: string, value: string): Promise<ResultT<void>> {
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
try {
this.storage!.setItem(key, value);
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async getJson<T>(key: string, schema: ZodType<T>): Promise<ResultT<T | null>> {
const r = await this.getString(key);
if (!r.success) return Result.err(r.error);
if (r.data === null) {
const value: T | null = null;
return Result.ok(value);
}
try {
const parsed = schema.parse(JSON.parse(r.data));
return Result.ok(parsed);
} catch (e) {
return Result.err(e);
}
}
async setJson<T>(
key: string,
value: T,
schema?: ZodType<T>,
): Promise<ResultT<void>> {
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
try {
const validated = schema ? schema.parse(value) : value;
this.storage!.setItem(key, JSON.stringify(validated));
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async remove(key: string): Promise<ResultT<void>> {
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
try {
this.storage!.removeItem(key);
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async has(key: string): Promise<ResultT<boolean>> {
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
try {
return Result.ok(this.storage!.getItem(key) !== null);
} catch (e) {
return Result.err(e);
}
}
}