From 904cb3638a5c9ae4c60f8a37ccf6e4f327efa556 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 9 Jun 2026 10:34:40 +0800 Subject: [PATCH] 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. --- src/data/repositories/auth_repository.ts | 16 +++---- src/data/repositories/chat_repository.ts | 24 ++++------- src/data/repositories/metrics_repository.ts | 2 +- src/data/repositories/user_repository.ts | 2 +- src/data/storage/app/app_storage.ts | 6 +-- src/data/storage/auth/auth_storage.ts | 14 +++---- src/data/storage/auth/iauth_storage.ts | 2 +- src/data/storage/chat/chat_storage.ts | 2 +- src/data/storage/chat/ichat_storage.ts | 2 +- src/data/storage/chat/local_chat_storage.ts | 42 +++++++++---------- src/data/storage/index.ts | 2 +- src/data/storage/local_storage.ts | 46 ++++++++++----------- src/data/storage/result.ts | 35 ---------------- src/data/storage/user/iuser_storage.ts | 2 +- src/data/storage/user/user_storage.ts | 6 +-- src/{data => utils}/result.ts | 0 16 files changed, 81 insertions(+), 122 deletions(-) delete mode 100644 src/data/storage/result.ts rename src/{data => utils}/result.ts (100%) diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index cbd9b63f..cd783bdc 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -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> { 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 { 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); } } diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 2f790534..58a4bc5f 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -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> { 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> { 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` 桥接到仓库层 - * `{success, data/error}` 形态。错误统一用 `Error` 包装。 + * 把 storage 层 `Result` 透传:仓库与 storage 现在共享同一全局 + * `Result`(来自 `@/utils/result`),不需要任何形状转换。 + * 该方法保留仅为未来万一又出现差异化时方便快速插入适配。 */ private async _mapLocalStorageResult( - promise: Promise< - | { readonly kind: "success"; readonly value: T } - | { readonly kind: "failure"; readonly error: unknown } - >, + promise: Promise>, ): Promise> { - const r = await promise; - if (r.kind === "success") { - return Result.ok(r.value); - } - return Result.err(r.error); + return await promise; } } diff --git a/src/data/repositories/metrics_repository.ts b/src/data/repositories/metrics_repository.ts index cc5fde81..e825bebb 100644 --- a/src/data/repositories/metrics_repository.ts +++ b/src/data/repositories/metrics_repository.ts @@ -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) {} diff --git a/src/data/repositories/user_repository.ts b/src/data/repositories/user_repository.ts index e3e1d204..1ad2401e 100644 --- a/src/data/repositories/user_repository.ts +++ b/src/data/repositories/user_repository.ts @@ -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) {} diff --git a/src/data/storage/app/app_storage.ts b/src/data/storage/app/app_storage.ts index c919a8c9..27d45761 100644 --- a/src/data/storage/app/app_storage.ts +++ b/src/data/storage/app/app_storage.ts @@ -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> { 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); } } diff --git a/src/data/storage/auth/auth_storage.ts b/src/data/storage/auth/auth_storage.ts index d66f442d..1dfcf6c2 100644 --- a/src/data/storage/auth/auth_storage.ts +++ b/src/data/storage/auth/auth_storage.ts @@ -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> { 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> { return this.ls.remove(StorageKeys.loginToken); @@ -64,8 +64,8 @@ export class AuthStorage implements IAuthStorage { } async hasGuestToken(): Promise> { 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> { return this.ls.remove(StorageKeys.guestToken); @@ -107,9 +107,9 @@ export class AuthStorage implements IAuthStorage { async clearAuthData(): Promise> { 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(); } } diff --git a/src/data/storage/auth/iauth_storage.ts b/src/data/storage/auth/iauth_storage.ts index 5d0aa6fb..7c9705bb 100644 --- a/src/data/storage/auth/iauth_storage.ts +++ b/src/data/storage/auth/iauth_storage.ts @@ -9,7 +9,7 @@ * - 所有方法返回 `Promise>`,与 Dart `Future>` 对齐 */ -import type { Result } from "../result"; +import type { Result } from "@/utils/result"; export interface IAuthStorage { getLoginToken(): Promise>; diff --git a/src/data/storage/chat/chat_storage.ts b/src/data/storage/chat/chat_storage.ts index 11da3eed..77d9e6d9 100644 --- a/src/data/storage/chat/chat_storage.ts +++ b/src/data/storage/chat/chat_storage.ts @@ -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"; diff --git a/src/data/storage/chat/ichat_storage.ts b/src/data/storage/chat/ichat_storage.ts index 589c2f98..85e50b22 100644 --- a/src/data/storage/chat/ichat_storage.ts +++ b/src/data/storage/chat/ichat_storage.ts @@ -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 { diff --git a/src/data/storage/chat/local_chat_storage.ts b/src/data/storage/chat/local_chat_storage.ts index f71947ae..a782c56b 100644 --- a/src/data/storage/chat/local_chat_storage.ts +++ b/src/data/storage/chat/local_chat_storage.ts @@ -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> { 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> { 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> { 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> { 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> { 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> { try { this.db.close(); - return Result.success(undefined); + return Result.ok(undefined); } catch (e) { - return Result.failure(e); + return Result.err(e); } } } diff --git a/src/data/storage/index.ts b/src/data/storage/index.ts index 8722c3be..f15fa816 100644 --- a/src/data/storage/index.ts +++ b/src/data/storage/index.ts @@ -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"; diff --git a/src/data/storage/local_storage.ts b/src/data/storage/local_storage.ts index 7fa80a66..ab0d400a 100644 --- a/src/data/storage/local_storage.ts +++ b/src/data/storage/local_storage.ts @@ -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> { - 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> { - 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(key: string, schema: ZodType): Promise> { 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, ): Promise> { - 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> { - 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> { - 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); } } } diff --git a/src/data/storage/result.ts b/src/data/storage/result.ts deleted file mode 100644 index 09d25786..00000000 --- a/src/data/storage/result.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 通用 Result 类型 - * - * 对齐 Dart 项目中的 `sealed class Result` / `Success` / `Failure` 模式。 - * TypeScript 没有 sealed class,用判别联合(discriminated union)实现等价效果: - * 调用方在 `switch (result.kind)` 上做穷尽匹配,编译器在 strict 模式下能保证完整性。 - * - * `error` 字段保持 `unknown`(不强制 `Error`)以匹配 Dart 端可抛任意对象的能力。 - */ - -export type Result = - | { readonly kind: "success"; readonly value: T } - | { readonly kind: "failure"; readonly error: unknown }; - -export const Result = { - success(value: T): Result { - return { kind: "success", value }; - }, - - failure(error: unknown): Result { - return { kind: "failure", error }; - }, - - isSuccess(r: Result): r is { kind: "success"; value: T } { - return r.kind === "success"; - }, - - isFailure(r: Result): r is { kind: "failure"; error: unknown } { - return r.kind === "failure"; - }, - - map(r: Result, fn: (v: T) => U): Result { - return r.kind === "success" ? Result.success(fn(r.value)) : r; - }, -} as const; diff --git a/src/data/storage/user/iuser_storage.ts b/src/data/storage/user/iuser_storage.ts index 2c6a1103..c0f7b9ae 100644 --- a/src/data/storage/user/iuser_storage.ts +++ b/src/data/storage/user/iuser_storage.ts @@ -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 { diff --git a/src/data/storage/user/user_storage.ts b/src/data/storage/user/user_storage.ts index 52d15558..146025e2 100644 --- a/src/data/storage/user/user_storage.ts +++ b/src/data/storage/user/user_storage.ts @@ -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> { 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(); } } diff --git a/src/data/result.ts b/src/utils/result.ts similarity index 100% rename from src/data/result.ts rename to src/utils/result.ts