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:
2026-06-09 10:34:40 +08:00
parent c767322db6
commit 904cb3638a
16 changed files with 81 additions and 122 deletions
+3 -3
View File
@@ -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);
}
}
+7 -7
View File
@@ -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();
}
}
+1 -1
View File
@@ -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>>;
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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 {
+21 -21
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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";
+23 -23
View File
@@ -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);
}
}
}
-35
View File
@@ -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;
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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();
}
}