240 lines
6.9 KiB
TypeScript
240 lines
6.9 KiB
TypeScript
"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<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>> {
|
||
try {
|
||
const value = await SpAsyncUtil._storage.getItem<unknown>(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<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));
|
||
}
|
||
|
||
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() });
|
||
}
|
||
}
|