"use client"; /** * SpAsyncUtil — 键值对持久化工具类(基于 unstorage) * * 用法: * ```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 memoryDriver from "unstorage/drivers/memory"; import type { ZodType } from "zod"; import { Result, type Result as ResultT } from "@/utils/result"; export class SpAsyncUtil { // ===== 私有静态状态 ===== // 环境判断 + 安全初始化:SSR 用 memory driver,浏览器用 localStorage driver。 // unstorage 1.10+ 要求显式传入 localStorage,故这里同步注入 window.localStorage。 private static _storage: Storage = createDefaultStorage(); // 私有构造函数:禁止实例化(对齐 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> { try { const value = await SpAsyncUtil._storage.getItem(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> { 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> { try { const value = await SpAsyncUtil._storage.getItem(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> { 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> { try { const value = await SpAsyncUtil._storage.getItem(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> { 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> { try { const value = await SpAsyncUtil._storage.getItem(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> { 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> { try { const value = await SpAsyncUtil._storage.getItem(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> { 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( key: string, schema: ZodType, ): Promise> { try { const value = await SpAsyncUtil._storage.getItem(key); if (value === null || value === undefined) return Result.ok(null); const json = typeof value === "string" ? JSON.parse(value) : value; const parsed = schema.parse(json); return Result.ok(parsed); } catch (e) { return Result.err(toError(e)); } } static async setJson( key: string, value: T, schema?: ZodType, ): Promise> { 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> { try { await SpAsyncUtil._storage.removeItem(key); return Result.ok(undefined); } catch (e) { return Result.err(toError(e)); } } static async clear(): Promise> { try { await SpAsyncUtil._storage.clear(); return Result.ok(undefined); } catch (e) { return Result.err(toError(e)); } } static async has(key: string): Promise> { 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)); } function createDefaultStorage(): Storage { if (typeof window === "undefined") { return createStorage({ driver: memoryDriver() }); } try { return createStorage({ driver: localStorageDriver({ base: "cozsweet:", localStorage: window.localStorage, window, }), }); } catch { return createStorage({ driver: memoryDriver() }); } }