refactor(data): move Result utility to utils and simplify API
Relocate the Result type from `@/data/result` to `@/utils/result` and replace the discriminated-union API (`kind`/`value`) with a simpler boolean-based API (`success`/`data`) across all repositories and storage classes for improved readability and consistency.
This commit is contained in:
@@ -26,7 +26,7 @@ import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
||||
import { RegisterRequest } from "@/data/dto/auth/register_request";
|
||||
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
|
||||
import { User } from "@/data/dto/user/user";
|
||||
import { Result } from "@/data/result";
|
||||
import { Result } from "@/utils/result";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
|
||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||
@@ -204,10 +204,10 @@ export class AuthRepository {
|
||||
*/
|
||||
async refreshToken(): Promise<Result<RefreshTokenResponse>> {
|
||||
const existing = await this.storage.getRefreshToken();
|
||||
if (existing.kind !== "success" || !existing.value) {
|
||||
if (!existing.success || !existing.data) {
|
||||
return Result.err(new Error("No refresh token available"));
|
||||
}
|
||||
const refreshToken = existing.value;
|
||||
const refreshToken = existing.data;
|
||||
return Result.wrap(async () => {
|
||||
const response = await this.api.refreshToken(
|
||||
RefreshTokenRequest.from({ refreshToken }),
|
||||
@@ -228,7 +228,7 @@ export class AuthRepository {
|
||||
return Result.wrap(async () => {
|
||||
const user = await this.api.getCurrentUser();
|
||||
const writeResult = await this.userStorage.setUser(user.toJson());
|
||||
if (writeResult.kind === "failure") {
|
||||
if (!writeResult.success) {
|
||||
console.warn(
|
||||
"[AuthRepository] failed to cache current user",
|
||||
writeResult.error,
|
||||
@@ -261,21 +261,21 @@ export class AuthRepository {
|
||||
*/
|
||||
private async _saveLoginData(data: LoginResponse): Promise<void> {
|
||||
const r1 = await this.storage.setLoginToken(data.token);
|
||||
if (r1.kind === "failure") {
|
||||
if (!r1.success) {
|
||||
console.warn("[AuthRepository] setLoginToken failed", r1.error);
|
||||
}
|
||||
if (data.refreshToken) {
|
||||
const r2 = await this.storage.setRefreshToken(data.refreshToken);
|
||||
if (r2.kind === "failure") {
|
||||
if (!r2.success) {
|
||||
console.warn("[AuthRepository] setRefreshToken failed", r2.error);
|
||||
}
|
||||
}
|
||||
const r3 = await this.userStorage.setUser(data.user.toJson());
|
||||
if (r3.kind === "failure") {
|
||||
if (!r3.success) {
|
||||
console.warn("[AuthRepository] setUser failed", r3.error);
|
||||
}
|
||||
const r4 = await this.userStorage.setUserId(data.user.id);
|
||||
if (r4.kind === "failure") {
|
||||
if (!r4.success) {
|
||||
console.warn("[AuthRepository] setUserId failed", r4.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
|
||||
import { ChatMessage } from "@/data/dto/chat/chat_message";
|
||||
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
|
||||
import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
|
||||
import { Result } from "@/data/result";
|
||||
import { Result } from "@/utils/result";
|
||||
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
|
||||
import { LocalMessage } from "@/data/storage/chat/local_message";
|
||||
|
||||
@@ -72,7 +72,7 @@ export class ChatRepository {
|
||||
messages: readonly ChatMessage[],
|
||||
): Promise<Result<void>> {
|
||||
const cleared = await this.localStorage.clearAll();
|
||||
if (cleared.kind === "failure") {
|
||||
if (!cleared.success) {
|
||||
return Result.err(cleared.error);
|
||||
}
|
||||
return this._mapLocalStorageResult(
|
||||
@@ -83,10 +83,10 @@ export class ChatRepository {
|
||||
/** 读取所有本地消息,按 dbId 升序。 */
|
||||
async getLocalMessages(): Promise<Result<ChatMessage[]>> {
|
||||
const result = await this.localStorage.getAllMessages();
|
||||
if (result.kind === "failure") {
|
||||
if (!result.success) {
|
||||
return Result.err(result.error);
|
||||
}
|
||||
return Result.ok(result.value.map((lm) => this._localToChat(lm)));
|
||||
return Result.ok(result.data.map((lm) => this._localToChat(lm)));
|
||||
}
|
||||
|
||||
/** 清空本地消息。 */
|
||||
@@ -126,20 +126,14 @@ export class ChatRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 storage 层 `{kind, value/error}` 形态的 `Result<T>` 桥接到仓库层
|
||||
* `{success, data/error}` 形态。错误统一用 `Error` 包装。
|
||||
* 把 storage 层 `Result<T>` 透传:仓库与 storage 现在共享同一全局
|
||||
* `Result<T>`(来自 `@/utils/result`),不需要任何形状转换。
|
||||
* 该方法保留仅为未来万一又出现差异化时方便快速插入适配。
|
||||
*/
|
||||
private async _mapLocalStorageResult<T>(
|
||||
promise: Promise<
|
||||
| { readonly kind: "success"; readonly value: T }
|
||||
| { readonly kind: "failure"; readonly error: unknown }
|
||||
>,
|
||||
promise: Promise<Result<T>>,
|
||||
): Promise<Result<T>> {
|
||||
const r = await promise;
|
||||
if (r.kind === "success") {
|
||||
return Result.ok(r.value);
|
||||
}
|
||||
return Result.err(r.error);
|
||||
return await promise;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { MetricsApi, metricsApi } from "@/data/api";
|
||||
import { AppEvent } from "@/data/dto/metrics/app_event";
|
||||
import { PwaEvent } from "@/data/dto/metrics/pwa_event";
|
||||
import { Result } from "@/data/result";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
export class MetricsRepository {
|
||||
constructor(private readonly api: MetricsApi) {}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
|
||||
import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
|
||||
import { User } from "@/data/dto/user/user";
|
||||
import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
|
||||
import { Result } from "@/data/result";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
export class UserRepository {
|
||||
constructor(private readonly api: UserApi) {}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* 仓库层统一 Result<T>
|
||||
*
|
||||
* 与 `src/data/storage/result.ts` 的 `Result<T>`(`{kind, value}` 形态)并存:
|
||||
* - 本类型供仓库层(`src/data/repositories/*`)使用,`{success, data}` 形态更贴近
|
||||
* 原 Dart `Result.success` / `Result.failure` 命名与 API 端 envelope 风格。
|
||||
* - 存储层继续使用 `{kind, value}`,因为它的消费者(localStorage、Dexie)已经在
|
||||
* 大量代码中 pattern-match `r.kind`。
|
||||
* - 两个类型路径不同(`@/data/result` vs `@/data/storage/result`),不冲突;
|
||||
* 如需在同模块中混用,可用 `import { Result as StorageResult } from
|
||||
* "@/data/storage/result"` 别名。
|
||||
*
|
||||
* `error` 固定为 `Error`(非 `unknown`):HTTP 拦截器与共享 unwrap 都抛 `ApiError`
|
||||
* (`ApiError extends Error`),仓库 `Result.wrap` 捕获后用 `toError` 规范化,
|
||||
* 调用方能可靠地 `instanceof ApiError` / `error.message` 访问错误信息。
|
||||
*/
|
||||
export type Result<T> =
|
||||
| { readonly success: true; readonly data: T }
|
||||
| { readonly success: false; readonly error: Error };
|
||||
|
||||
export const Result = {
|
||||
/** 构造成功结果。 */
|
||||
ok<T>(data: T): Result<T> {
|
||||
return { success: true, data };
|
||||
},
|
||||
|
||||
/**
|
||||
* 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `Error`。
|
||||
*/
|
||||
err<T = never>(error: unknown): Result<T> {
|
||||
return { success: false, error: toError(error) };
|
||||
},
|
||||
|
||||
/** 类型守卫:true 分支。 */
|
||||
isOk<T>(r: Result<T>): r is { readonly success: true; readonly data: T } {
|
||||
return r.success;
|
||||
},
|
||||
|
||||
/** 类型守卫:false 分支。 */
|
||||
isErr<T>(r: Result<T>): r is { readonly success: false; readonly error: Error } {
|
||||
return !r.success;
|
||||
},
|
||||
|
||||
/** 不可变的链式 map:失败时原样传递。 */
|
||||
map<T, U>(r: Result<T>, fn: (v: T) => U): Result<U> {
|
||||
return r.success ? Result.ok(fn(r.data)) : r;
|
||||
},
|
||||
|
||||
/**
|
||||
* 捕获异步 thunk 的异常并包装成 `Result<T>`。仓库内主要用这个把
|
||||
* `*Api` 调用从「throw 风格」桥接到「Result 风格」。
|
||||
*/
|
||||
async wrap<T>(thunk: () => Promise<T>): Promise<Result<T>> {
|
||||
try {
|
||||
return Result.ok(await thunk());
|
||||
} catch (e) {
|
||||
return Result.err(e);
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 把任意 thrown 值规范化为 `Error`:
|
||||
* - 已是 `Error` 子类 → 原样返回(保留 `.code` / `.status` 等自定义字段)
|
||||
* - string → `new Error(str)`
|
||||
* - 其他 → `new Error(JSON.stringify(value))`,失败则 `new Error(String(value))`
|
||||
*/
|
||||
export function toError(value: unknown): Error {
|
||||
if (value instanceof Error) return value;
|
||||
if (typeof value === "string") return new Error(value);
|
||||
try {
|
||||
return new Error(JSON.stringify(value));
|
||||
} catch {
|
||||
return new Error(String(value));
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { LocalStorage } from "../local_storage";
|
||||
import { Result, type Result as ResultT } from "../result";
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
|
||||
export class AppStorage {
|
||||
@@ -78,7 +78,7 @@ export class AppStorage {
|
||||
todayString: string,
|
||||
): Promise<ResultT<boolean>> {
|
||||
const r = await AppStorage.ls.getString(key);
|
||||
if (r.kind === "failure") return r;
|
||||
return Result.success(r.value === null || r.value !== todayString);
|
||||
if (!r.success) return r;
|
||||
return Result.ok(r.data === null || r.data !== todayString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { LocalStorage } from "../local_storage";
|
||||
import { Result, type Result as ResultT } from "../result";
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
import type { IAuthStorage } from "./iauth_storage";
|
||||
|
||||
@@ -47,8 +47,8 @@ export class AuthStorage implements IAuthStorage {
|
||||
}
|
||||
async hasLoginToken(): Promise<ResultT<boolean>> {
|
||||
const r = await this.getLoginToken();
|
||||
if (r.kind === "failure") return Result.failure(r.error);
|
||||
return Result.success(r.value !== null && r.value.length > 0);
|
||||
if (!r.success) return Result.err(r.error);
|
||||
return Result.ok(r.data !== null && r.data.length > 0);
|
||||
}
|
||||
clearLoginToken(): Promise<ResultT<void>> {
|
||||
return this.ls.remove(StorageKeys.loginToken);
|
||||
@@ -64,8 +64,8 @@ export class AuthStorage implements IAuthStorage {
|
||||
}
|
||||
async hasGuestToken(): Promise<ResultT<boolean>> {
|
||||
const r = await this.getGuestToken();
|
||||
if (r.kind === "failure") return Result.failure(r.error);
|
||||
return Result.success(r.value !== null && r.value.length > 0);
|
||||
if (!r.success) return Result.err(r.error);
|
||||
return Result.ok(r.data !== null && r.data.length > 0);
|
||||
}
|
||||
clearGuestToken(): Promise<ResultT<void>> {
|
||||
return this.ls.remove(StorageKeys.guestToken);
|
||||
@@ -107,9 +107,9 @@ export class AuthStorage implements IAuthStorage {
|
||||
|
||||
async clearAuthData(): Promise<ResultT<void>> {
|
||||
const r1 = await this.clearLoginToken();
|
||||
if (r1.kind === "failure") return r1;
|
||||
if (!r1.success) return r1;
|
||||
const r2 = await this.clearGuestToken();
|
||||
if (r2.kind === "failure") return r2;
|
||||
if (!r2.success) return r2;
|
||||
return this.clearRefreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* - 所有方法返回 `Promise<Result<T>>`,与 Dart `Future<Result<T>>` 对齐
|
||||
*/
|
||||
|
||||
import type { Result } from "../result";
|
||||
import type { Result } from "@/utils/result";
|
||||
|
||||
export interface IAuthStorage {
|
||||
getLoginToken(): Promise<Result<string | null>>;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { z } from "zod";
|
||||
|
||||
import { LocalStorage } from "../local_storage";
|
||||
import { type Result as ResultT } from "../result";
|
||||
import { type Result as ResultT } from "@/utils/result";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
import type { IChatStorage } from "./ichat_storage";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 跨天重置逻辑(`needsReset`)保留在 `GuestChatQuota` 类上,存储层只做读写。
|
||||
*/
|
||||
|
||||
import type { Result } from "../result";
|
||||
import type { Result } from "@/utils/result";
|
||||
import type { GuestChatQuotaData } from "@/data/schemas/chat/guest_chat_quota";
|
||||
|
||||
export interface IChatStorage {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* 数据量大时考虑升级 schema 加索引。
|
||||
*/
|
||||
|
||||
import { Result, type Result as ResultT } from "../result";
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
import { LocalChatDB } from "./local_chat_db";
|
||||
import { LocalMessage } from "./local_message";
|
||||
|
||||
@@ -47,9 +47,9 @@ export class LocalChatStorage {
|
||||
async init(): Promise<ResultT<void>> {
|
||||
try {
|
||||
await this.db.open();
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ export class LocalChatStorage {
|
||||
async saveMessage(message: LocalMessage): Promise<ResultT<void>> {
|
||||
try {
|
||||
await this.db.messages.add(message.toRow());
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ export class LocalChatStorage {
|
||||
}
|
||||
},
|
||||
);
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,17 +88,17 @@ export class LocalChatStorage {
|
||||
const rows = await this.db.messages.toArray();
|
||||
// 按 dbId 升序(与 Dart `box.values.toList()` 插入序语义一致)
|
||||
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
|
||||
return Result.success(rows.map((r) => LocalMessage.fromRow(r)));
|
||||
return Result.ok(rows.map((r) => LocalMessage.fromRow(r)));
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getMessageCount(): Promise<ResultT<number>> {
|
||||
try {
|
||||
return Result.success(await this.db.messages.count());
|
||||
return Result.ok(await this.db.messages.count());
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@ export class LocalChatStorage {
|
||||
.filter((r) => r.sessionId === sessionId)
|
||||
.toArray();
|
||||
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
|
||||
return Result.success(rows.map((r) => LocalMessage.fromRow(r)));
|
||||
return Result.ok(rows.map((r) => LocalMessage.fromRow(r)));
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,9 @@ export class LocalChatStorage {
|
||||
async clearAll(): Promise<ResultT<void>> {
|
||||
try {
|
||||
await this.db.messages.clear();
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,30 +134,30 @@ export class LocalChatStorage {
|
||||
async deleteMessage(index: number): Promise<ResultT<void>> {
|
||||
try {
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
return Result.failure(
|
||||
return Result.err(
|
||||
new RangeError(`deleteMessage: index ${index} out of range`),
|
||||
);
|
||||
}
|
||||
const rows = await this.db.messages.toArray();
|
||||
if (index >= rows.length) {
|
||||
return Result.failure(
|
||||
return Result.err(
|
||||
new RangeError(`deleteMessage: index ${index} out of range`),
|
||||
);
|
||||
}
|
||||
const target = rows[index]!;
|
||||
await this.db.messages.delete(target.dbId!);
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<ResultT<void>> {
|
||||
try {
|
||||
this.db.close();
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
export * from "./auth_storage";
|
||||
export * from "./local_storage";
|
||||
export * from "./result";
|
||||
export * from "@/utils/result";
|
||||
export * from "./storage_keys";
|
||||
export * from "./app/app_storage";
|
||||
export * from "./auth/auth_storage";
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { type ZodType } from "zod";
|
||||
import { Result, type Result as ResultT } from "./result";
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
|
||||
const SSR_ERROR_MSG =
|
||||
"localStorage is not available in this environment (SSR or non-browser)";
|
||||
@@ -52,38 +52,38 @@ export class LocalStorage {
|
||||
}
|
||||
|
||||
async getString(key: string): Promise<ResultT<string | null>> {
|
||||
if (!this.isAvailable()) return Result.failure(new Error(SSR_ERROR_MSG));
|
||||
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
|
||||
try {
|
||||
const v = this.storage!.getItem(key);
|
||||
if (v === null || v === "") return Result.success(null);
|
||||
return Result.success(v);
|
||||
if (v === null || v === "") return Result.ok(null);
|
||||
return Result.ok(v);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
async setString(key: string, value: string): Promise<ResultT<void>> {
|
||||
if (!this.isAvailable()) return Result.failure(new Error(SSR_ERROR_MSG));
|
||||
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
|
||||
try {
|
||||
this.storage!.setItem(key, value);
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(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.kind === "failure") return Result.failure(r.error);
|
||||
if (r.value === null) {
|
||||
if (!r.success) return Result.err(r.error);
|
||||
if (r.data === null) {
|
||||
const value: T | null = null;
|
||||
return Result.success(value);
|
||||
return Result.ok(value);
|
||||
}
|
||||
try {
|
||||
const parsed = schema.parse(JSON.parse(r.value));
|
||||
return Result.success(parsed);
|
||||
const parsed = schema.parse(JSON.parse(r.data));
|
||||
return Result.ok(parsed);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,32 +92,32 @@ export class LocalStorage {
|
||||
value: T,
|
||||
schema?: ZodType<T>,
|
||||
): Promise<ResultT<void>> {
|
||||
if (!this.isAvailable()) return Result.failure(new Error(SSR_ERROR_MSG));
|
||||
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.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<ResultT<void>> {
|
||||
if (!this.isAvailable()) return Result.failure(new Error(SSR_ERROR_MSG));
|
||||
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
|
||||
try {
|
||||
this.storage!.removeItem(key);
|
||||
return Result.success(undefined);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
async has(key: string): Promise<ResultT<boolean>> {
|
||||
if (!this.isAvailable()) return Result.failure(new Error(SSR_ERROR_MSG));
|
||||
if (!this.isAvailable()) return Result.err(new Error(SSR_ERROR_MSG));
|
||||
try {
|
||||
return Result.success(this.storage!.getItem(key) !== null);
|
||||
return Result.ok(this.storage!.getItem(key) !== null);
|
||||
} catch (e) {
|
||||
return Result.failure(e);
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* 通用 Result<T> 类型
|
||||
*
|
||||
* 对齐 Dart 项目中的 `sealed class Result<T>` / `Success<T>` / `Failure<T>` 模式。
|
||||
* TypeScript 没有 sealed class,用判别联合(discriminated union)实现等价效果:
|
||||
* 调用方在 `switch (result.kind)` 上做穷尽匹配,编译器在 strict 模式下能保证完整性。
|
||||
*
|
||||
* `error` 字段保持 `unknown`(不强制 `Error`)以匹配 Dart 端可抛任意对象的能力。
|
||||
*/
|
||||
|
||||
export type Result<T> =
|
||||
| { readonly kind: "success"; readonly value: T }
|
||||
| { readonly kind: "failure"; readonly error: unknown };
|
||||
|
||||
export const Result = {
|
||||
success<T>(value: T): Result<T> {
|
||||
return { kind: "success", value };
|
||||
},
|
||||
|
||||
failure<T = never>(error: unknown): Result<T> {
|
||||
return { kind: "failure", error };
|
||||
},
|
||||
|
||||
isSuccess<T>(r: Result<T>): r is { kind: "success"; value: T } {
|
||||
return r.kind === "success";
|
||||
},
|
||||
|
||||
isFailure<T>(r: Result<T>): r is { kind: "failure"; error: unknown } {
|
||||
return r.kind === "failure";
|
||||
},
|
||||
|
||||
map<T, U>(r: Result<T>, fn: (v: T) => U): Result<U> {
|
||||
return r.kind === "success" ? Result.success(fn(r.value)) : r;
|
||||
},
|
||||
} as const;
|
||||
@@ -11,7 +11,7 @@
|
||||
* 具体序列化由实现层通过 Zod schema 校验后转换为 `User` 类实例。
|
||||
*/
|
||||
|
||||
import type { Result } from "../result";
|
||||
import type { Result } from "@/utils/result";
|
||||
import type { UserData } from "@/data/schemas/user/user";
|
||||
|
||||
export interface IUserStorage {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import { UserSchema, type UserData } from "@/data/schemas/user/user";
|
||||
import { LocalStorage } from "../local_storage";
|
||||
import { type Result as ResultT } from "../result";
|
||||
import { type Result as ResultT } from "@/utils/result";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
import type { IUserStorage } from "./iuser_storage";
|
||||
|
||||
@@ -85,9 +85,9 @@ export class UserStorage implements IUserStorage {
|
||||
|
||||
async clearUserData(): Promise<ResultT<void>> {
|
||||
const r1 = await this.clearUser();
|
||||
if (r1.kind === "failure") return r1;
|
||||
if (!r1.success) return r1;
|
||||
const r2 = await this.clearUserId();
|
||||
if (r2.kind === "failure") return r2;
|
||||
if (!r2.success) return r2;
|
||||
return this.clearAvatarUrl();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user