chore(deps): add unstorage and migrate ChatStorage to use SpAsyncUtil
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
"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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* SpAsyncUtil — 键值对持久化工具类(基于 unstorage)
|
||||
*
|
||||
* 原始 Dart: `lib/utils/src/sp_async_util.dart`(`SpAsyncUtil` 静态类)
|
||||
*
|
||||
* 设计目标:
|
||||
* - **完全静态类化**:所有状态/方法/函数均封装在 SpAsyncUtil 类内
|
||||
* - 底层基于 unstorage 抽象(浏览器 localStorage / SSR memory 自动切换)
|
||||
* - 保留 `Promise<Result<T>>` API 兼容(state machines、interceptor、repositories 零修改)
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* import { SpAsyncUtil } from "@/utils/storage";
|
||||
*
|
||||
* const r = await SpAsyncUtil.getString("auth_token");
|
||||
* if (r.success && r.data) { ... }
|
||||
* ```
|
||||
*/
|
||||
import { createStorage, type Storage } from "unstorage";
|
||||
import localStorageDriver from "unstorage/drivers/localstorage";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
|
||||
export class SpAsyncUtil {
|
||||
// ===== 私有静态状态 =====
|
||||
private static _storage: Storage = createStorage({
|
||||
driver: localStorageDriver({ base: "cozsweet:" }),
|
||||
});
|
||||
|
||||
// 私有构造函数:禁止实例化(对齐 Dart `SpAsyncUtil._()`)
|
||||
private constructor() {}
|
||||
|
||||
// ===== 底层 storage 访问 =====
|
||||
|
||||
/**
|
||||
* 替换底层 storage 实例(仅测试/Storybook 用)
|
||||
* @internal
|
||||
*/
|
||||
static setStorage(instance: Storage): void {
|
||||
SpAsyncUtil._storage = instance;
|
||||
}
|
||||
|
||||
/** 获取底层 storage 实例(高级用法) */
|
||||
static getRawStorage(): Storage {
|
||||
return SpAsyncUtil._storage;
|
||||
}
|
||||
|
||||
// ===== string =====
|
||||
static async getString(key: string): Promise<ResultT<string | null>> {
|
||||
try {
|
||||
const value = await SpAsyncUtil._storage.getItem<string>(key);
|
||||
if (value === null || value === undefined) return Result.ok(null);
|
||||
if (typeof value !== "string") return Result.ok(null);
|
||||
if (value.length === 0) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setString(
|
||||
key: string,
|
||||
value: string,
|
||||
): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== int =====
|
||||
static async getInt(key: string): Promise<ResultT<number | null>> {
|
||||
try {
|
||||
const value = await SpAsyncUtil._storage.getItem<number>(key);
|
||||
if (value === null || value === undefined) return Result.ok(null);
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null);
|
||||
return Result.ok(Math.trunc(value));
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setInt(key: string, value: number): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== double =====
|
||||
static async getDouble(key: string): Promise<ResultT<number | null>> {
|
||||
try {
|
||||
const value = await SpAsyncUtil._storage.getItem<number>(key);
|
||||
if (value === null || value === undefined) return Result.ok(null);
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setDouble(key: string, value: number): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== bool =====
|
||||
static async getBool(key: string): Promise<ResultT<boolean | null>> {
|
||||
try {
|
||||
const value = await SpAsyncUtil._storage.getItem<boolean>(key);
|
||||
if (value === null || value === undefined) return Result.ok(null);
|
||||
if (typeof value !== "boolean") return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setBool(key: string, value: boolean): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== string list =====
|
||||
static async getStringList(key: string): Promise<ResultT<string[] | null>> {
|
||||
try {
|
||||
const value = await SpAsyncUtil._storage.getItem<string[]>(key);
|
||||
if (value === null || value === undefined) return Result.ok(null);
|
||||
if (!Array.isArray(value)) return Result.ok(null);
|
||||
if (!value.every((v) => typeof v === "string")) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setStringList(
|
||||
key: string,
|
||||
value: string[],
|
||||
): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== JSON (with optional Zod validation) =====
|
||||
static async getJson<T>(
|
||||
key: string,
|
||||
schema: ZodType<T>,
|
||||
): Promise<ResultT<T | null>> {
|
||||
const r = await SpAsyncUtil.getString(key);
|
||||
if (!r.success) return Result.err(r.error);
|
||||
if (r.data === null) return Result.ok(null);
|
||||
try {
|
||||
const parsed = schema.parse(JSON.parse(r.data));
|
||||
return Result.ok(parsed);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async setJson<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
schema?: ZodType<T>,
|
||||
): Promise<ResultT<void>> {
|
||||
try {
|
||||
const validated = schema ? schema.parse(value) : value;
|
||||
await SpAsyncUtil._storage.setItem(key, JSON.stringify(validated));
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== generic operations =====
|
||||
static async remove(key: string): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.removeItem(key);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async clear(): Promise<ResultT<void>> {
|
||||
try {
|
||||
await SpAsyncUtil._storage.clear();
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async has(key: string): Promise<ResultT<boolean>> {
|
||||
try {
|
||||
const exists = await SpAsyncUtil._storage.hasItem(key);
|
||||
return Result.ok(exists);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 内部辅助
|
||||
// ============================================================
|
||||
function toError(e: unknown): Error {
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
Reference in New Issue
Block a user