diff --git a/src/core/errors/__tests__/exception-handler.test.ts b/src/core/errors/__tests__/exception-handler.test.ts new file mode 100644 index 00000000..50e83dd9 --- /dev/null +++ b/src/core/errors/__tests__/exception-handler.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { ApiError, ErrorCode } from "@/data/services/api/api_result"; + +import { AppException } from "../app-exception"; +import { ExceptionCode } from "../exception-code"; +import { ExceptionHandler } from "../exception-handler"; + +describe("ExceptionHandler", () => { + it("maps API status errors to stable user messages", () => { + expect( + ExceptionHandler.handle( + new ApiError("HTTP_UNAUTHORIZED", "raw backend message", 401), + ), + ).toEqual({ + code: ExceptionCode.unauthorized, + message: "Session expired, please sign in again.", + }); + + expect( + ExceptionHandler.handle( + new ApiError("HTTP_SERVER_ERROR", "internal", 500), + ), + ).toEqual({ + code: ExceptionCode.server, + message: "Server error, please try again later.", + }); + }); + + it("maps known API codes without status", () => { + expect( + ExceptionHandler.handle( + new ApiError(ErrorCode.timeoutError, "request timeout"), + ), + ).toEqual({ + code: ExceptionCode.timeout, + message: "request timeout", + }); + }); + + it("maps network and cancellation errors", () => { + expect(ExceptionHandler.handle(new TypeError("Failed to fetch"))).toEqual({ + code: ExceptionCode.network, + message: "Network connection failed.", + }); + + expect(ExceptionHandler.handle(new DOMException("Aborted", "AbortError"))).toEqual({ + code: ExceptionCode.cancelled, + message: "Request cancelled.", + }); + }); + + it("maps schema and syntax errors to validation errors", () => { + const parseResult = z.object({ name: z.string() }).safeParse({ name: null }); + if (parseResult.success) throw new Error("Expected parse failure"); + + expect(ExceptionHandler.handle(parseResult.error)).toEqual({ + code: ExceptionCode.validation, + message: "Data format changed unexpectedly. Please try again.", + }); + + expect(ExceptionHandler.handle(new SyntaxError("Unexpected token"))).toEqual({ + code: ExceptionCode.validation, + message: "Data format changed unexpectedly. Please try again.", + }); + }); + + it("maps storage and payment errors", () => { + expect(ExceptionHandler.handle(new Error("localStorage unavailable"))).toEqual({ + code: ExceptionCode.storage, + message: "Local data could not be saved. Please try again.", + }); + + expect(ExceptionHandler.handle(new Error("Stripe payment failed"))).toEqual({ + code: ExceptionCode.payment, + message: "Payment could not be completed. Please try again.", + }); + }); + + it("preserves an existing AppException result", () => { + const error = new AppException({ + code: ExceptionCode.notFound, + message: "Custom not found", + }); + + expect(ExceptionHandler.handle(error)).toEqual({ + code: ExceptionCode.notFound, + message: "Custom not found", + }); + }); + + it("falls back to a simple message", () => { + expect(ExceptionHandler.handle("plain failure")).toEqual({ + code: ExceptionCode.unknown, + message: "plain failure", + }); + + expect(ExceptionHandler.handle({ nope: true })).toEqual({ + code: ExceptionCode.unknown, + message: "Something went wrong. Please try again.", + }); + }); +}); diff --git a/src/core/errors/app-exception.ts b/src/core/errors/app-exception.ts new file mode 100644 index 00000000..6865a276 --- /dev/null +++ b/src/core/errors/app-exception.ts @@ -0,0 +1,21 @@ +import type { ExceptionResult } from "./exception-result"; +import type { ExceptionCode } from "./exception-code"; + +export class AppException extends Error { + readonly code: ExceptionCode; + override readonly cause?: unknown; + + constructor(result: ExceptionResult, cause?: unknown) { + super(result.message); + this.name = "AppException"; + this.code = result.code; + this.cause = cause; + } + + toResult(): ExceptionResult { + return { + code: this.code, + message: this.message, + }; + } +} diff --git a/src/core/errors/exception-code.ts b/src/core/errors/exception-code.ts new file mode 100644 index 00000000..c0bd732c --- /dev/null +++ b/src/core/errors/exception-code.ts @@ -0,0 +1,16 @@ +export const ExceptionCode = { + unknown: "UNKNOWN", + network: "NETWORK_ERROR", + timeout: "NETWORK_TIMEOUT", + unauthorized: "HTTP_UNAUTHORIZED", + forbidden: "HTTP_FORBIDDEN", + notFound: "HTTP_NOT_FOUND", + server: "HTTP_SERVER_ERROR", + validation: "VALIDATION_ERROR", + storage: "STORAGE_ERROR", + payment: "PAYMENT_ERROR", + cancelled: "CANCELLED", +} as const; + +export type ExceptionCode = + (typeof ExceptionCode)[keyof typeof ExceptionCode]; diff --git a/src/core/errors/exception-handler.ts b/src/core/errors/exception-handler.ts new file mode 100644 index 00000000..5748c667 --- /dev/null +++ b/src/core/errors/exception-handler.ts @@ -0,0 +1,47 @@ +import { AppException } from "./app-exception"; +import type { ExceptionResult } from "./exception-result"; +import { ApiExceptionHandler } from "./handlers/api-exception-handler"; +import { FallbackExceptionHandler } from "./handlers/fallback-exception-handler"; +import { NetworkExceptionHandler } from "./handlers/network-exception-handler"; +import { PaymentExceptionHandler } from "./handlers/payment-exception-handler"; +import { StorageExceptionHandler } from "./handlers/storage-exception-handler"; +import { ValidationExceptionHandler } from "./handlers/validation-exception-handler"; + +export class ExceptionHandler { + static handle(error: unknown): ExceptionResult { + if (error instanceof AppException) { + return error.toResult(); + } + + if (ApiExceptionHandler.canHandle(error)) { + return ApiExceptionHandler.handle(error); + } + + if (NetworkExceptionHandler.canHandle(error)) { + return NetworkExceptionHandler.handle(error); + } + + if (ValidationExceptionHandler.canHandle(error)) { + return ValidationExceptionHandler.handle(error); + } + + if (StorageExceptionHandler.canHandle(error)) { + return StorageExceptionHandler.handle(error); + } + + if (PaymentExceptionHandler.canHandle(error)) { + return PaymentExceptionHandler.handle(error); + } + + return FallbackExceptionHandler.handle(error); + } + + static message(error: unknown): string { + return ExceptionHandler.handle(error).message; + } + + static toError(error: unknown): AppException { + if (error instanceof AppException) return error; + return new AppException(ExceptionHandler.handle(error), error); + } +} diff --git a/src/core/errors/exception-result.ts b/src/core/errors/exception-result.ts new file mode 100644 index 00000000..a07e1096 --- /dev/null +++ b/src/core/errors/exception-result.ts @@ -0,0 +1,6 @@ +import type { ExceptionCode } from "./exception-code"; + +export interface ExceptionResult { + code: ExceptionCode; + message: string; +} diff --git a/src/core/errors/handlers/api-exception-handler.ts b/src/core/errors/handlers/api-exception-handler.ts new file mode 100644 index 00000000..f24875d1 --- /dev/null +++ b/src/core/errors/handlers/api-exception-handler.ts @@ -0,0 +1,57 @@ +import { ApiError, ErrorCode } from "@/data/services/api/api_result"; + +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class ApiExceptionHandler { + static canHandle(error: unknown): error is ApiError { + return error instanceof ApiError; + } + + static handle(error: ApiError): ExceptionResult { + switch (error.status) { + case ErrorCode.httpUnauthorized: + return { + code: ExceptionCode.unauthorized, + message: "Session expired, please sign in again.", + }; + case ErrorCode.httpForbidden: + return { + code: ExceptionCode.forbidden, + message: "Access denied.", + }; + case ErrorCode.httpNotFound: + return { + code: ExceptionCode.notFound, + message: "Resource not found.", + }; + case ErrorCode.httpServerError: + return { + code: ExceptionCode.server, + message: "Server error, please try again later.", + }; + default: + return { + code: mapApiCode(error.code), + message: error.message || "API call failed.", + }; + } + } +} + +function mapApiCode(code: string): ExceptionResult["code"] { + switch (code) { + case "HTTP_UNAUTHORIZED": + return ExceptionCode.unauthorized; + case "HTTP_SERVER_ERROR": + return ExceptionCode.server; + case ErrorCode.networkError: + return ExceptionCode.network; + case ErrorCode.timeoutError: + return ExceptionCode.timeout; + case ErrorCode.parseError: + return ExceptionCode.validation; + default: + return ExceptionCode.unknown; + } +} diff --git a/src/core/errors/handlers/fallback-exception-handler.ts b/src/core/errors/handlers/fallback-exception-handler.ts new file mode 100644 index 00000000..c5199a7e --- /dev/null +++ b/src/core/errors/handlers/fallback-exception-handler.ts @@ -0,0 +1,23 @@ +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class FallbackExceptionHandler { + static handle(error: unknown): ExceptionResult { + return { + code: ExceptionCode.unknown, + message: resolveMessage(error), + }; + } +} + +function resolveMessage(error: unknown): string { + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + + if (typeof error === "string" && error.trim().length > 0) { + return error; + } + + return "Something went wrong. Please try again."; +} diff --git a/src/core/errors/handlers/network-exception-handler.ts b/src/core/errors/handlers/network-exception-handler.ts new file mode 100644 index 00000000..822bc356 --- /dev/null +++ b/src/core/errors/handlers/network-exception-handler.ts @@ -0,0 +1,44 @@ +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class NetworkExceptionHandler { + static canHandle(error: unknown): error is Error | DOMException { + if (!isErrorLike(error)) return false; + const message = error.message.toLowerCase(); + return ( + error.name === "AbortError" || + error.name === "TimeoutError" || + message.includes("failed to fetch") || + message.includes("fetch failed") || + message.includes("network") || + message.includes("timeout") + ); + } + + static handle(error: Error | DOMException): ExceptionResult { + if (error.name === "AbortError") { + return { + code: ExceptionCode.cancelled, + message: "Request cancelled.", + }; + } + + const message = error.message.toLowerCase(); + if (error.name === "TimeoutError" || message.includes("timeout")) { + return { + code: ExceptionCode.timeout, + message: "Request timeout, please try again.", + }; + } + + return { + code: ExceptionCode.network, + message: "Network connection failed.", + }; + } +} + +function isErrorLike(error: unknown): error is Error | DOMException { + if (error instanceof Error) return true; + return typeof DOMException !== "undefined" && error instanceof DOMException; +} diff --git a/src/core/errors/handlers/payment-exception-handler.ts b/src/core/errors/handlers/payment-exception-handler.ts new file mode 100644 index 00000000..a8001b70 --- /dev/null +++ b/src/core/errors/handlers/payment-exception-handler.ts @@ -0,0 +1,30 @@ +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class PaymentExceptionHandler { + static canHandle(error: unknown): error is Error { + if (!(error instanceof Error)) return false; + const message = error.message.toLowerCase(); + return ( + message.includes("payment") || + message.includes("stripe") || + message.includes("ezpay") || + message.includes("order") + ); + } + + static handle(error: Error): ExceptionResult { + const message = error.message.toLowerCase(); + if (message.includes("cancel")) { + return { + code: ExceptionCode.cancelled, + message: "Payment was cancelled.", + }; + } + + return { + code: ExceptionCode.payment, + message: "Payment could not be completed. Please try again.", + }; + } +} diff --git a/src/core/errors/handlers/storage-exception-handler.ts b/src/core/errors/handlers/storage-exception-handler.ts new file mode 100644 index 00000000..d2fc719a --- /dev/null +++ b/src/core/errors/handlers/storage-exception-handler.ts @@ -0,0 +1,43 @@ +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class StorageExceptionHandler { + static canHandle(error: unknown): error is Error | DOMException { + if (!isErrorLike(error)) return false; + const message = error.message.toLowerCase(); + return ( + error.name === "QuotaExceededError" || + error.name === "SecurityError" || + message.includes("localstorage") || + message.includes("indexeddb") || + message.includes("quota") || + message.includes("storage") + ); + } + + static handle(error: Error | DOMException): ExceptionResult { + if (error.name === "QuotaExceededError") { + return { + code: ExceptionCode.storage, + message: "Storage quota exceeded.", + }; + } + + if (error.name === "SecurityError") { + return { + code: ExceptionCode.storage, + message: "Storage permission denied.", + }; + } + + return { + code: ExceptionCode.storage, + message: "Local data could not be saved. Please try again.", + }; + } +} + +function isErrorLike(error: unknown): error is Error | DOMException { + if (error instanceof Error) return true; + return typeof DOMException !== "undefined" && error instanceof DOMException; +} diff --git a/src/core/errors/handlers/validation-exception-handler.ts b/src/core/errors/handlers/validation-exception-handler.ts new file mode 100644 index 00000000..213aed78 --- /dev/null +++ b/src/core/errors/handlers/validation-exception-handler.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { ExceptionCode } from "../exception-code"; +import type { ExceptionResult } from "../exception-result"; + +export class ValidationExceptionHandler { + static canHandle(error: unknown): error is z.ZodError | SyntaxError { + return error instanceof z.ZodError || error instanceof SyntaxError; + } + + static handle(error?: z.ZodError | SyntaxError): ExceptionResult { + void error; + return { + code: ExceptionCode.validation, + message: "Data format changed unexpectedly. Please try again.", + }; + } +} diff --git a/src/core/errors/index.ts b/src/core/errors/index.ts new file mode 100644 index 00000000..cc717438 --- /dev/null +++ b/src/core/errors/index.ts @@ -0,0 +1,4 @@ +export * from "./app-exception"; +export * from "./exception-code"; +export * from "./exception-handler"; +export * from "./exception-result"; diff --git a/src/data/services/api/response_helper.ts b/src/data/services/api/response_helper.ts index ff58efe4..e2baded8 100644 --- a/src/data/services/api/response_helper.ts +++ b/src/data/services/api/response_helper.ts @@ -5,10 +5,8 @@ * 集中到这里。HTTP 拦截器(`http_client.ts`)已经对非 2xx 响应抛 `ApiError`; * 本工具处理第二种失败:2xx 但 `success: false` 的 envelope。 * - * 行为升级:失败时抛 `ApiError`(`ApiError extends Error`),而不是裸 `Error`。 - * 这样调用方在 `Result.err` 内能 `instanceof ApiError` 检查 `.code` / `.status`。 - * - * + * 失败时仍抛 `ApiError`,让底层保留 HTTP/API 上下文;进入仓库层 `Result` + * 后会被 `ExceptionHandler` 统一收敛为应用可消费的 `{ code, message }`。 */ import { ApiError, ErrorCode } from "./api_result"; diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 147d9df1..8ba1eb7f 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -3,6 +3,7 @@ */ import { setup, assign } from "xstate"; +import { ExceptionHandler } from "@/core/errors"; import type { AuthProvider } from "@/lib/auth/auth_platform"; import { AuthState, initialState } from "./auth-state"; @@ -23,6 +24,9 @@ export type { AuthState } from "./auth-state"; export { initialState } from "./auth-state"; export type { AuthEvent } from "./auth-events"; +const toAuthErrorMessage = (error: unknown): string => + ExceptionHandler.message(error); + // ============================================================ // Machine // @@ -119,8 +123,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, @@ -145,8 +148,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, @@ -174,8 +176,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, @@ -208,8 +209,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, @@ -231,8 +231,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), }), }, }, @@ -254,8 +253,7 @@ export const authMachine = setup({ actions: assign(({ event }) => ({ ...initialState, hasInitialized: true, - errorMessage: - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: toAuthErrorMessage(event.error), })), }, }, @@ -281,8 +279,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, @@ -309,8 +306,7 @@ export const authMachine = setup({ onError: { target: "idle", actions: assign({ - errorMessage: ({ event }) => - event.error instanceof Error ? event.error.message : String(event.error), + errorMessage: ({ event }) => toAuthErrorMessage(event.error), hasInitialized: true, }), }, diff --git a/src/stores/chat/chat-send-flow.ts b/src/stores/chat/chat-send-flow.ts index 4b6857c5..33d09434 100644 --- a/src/stores/chat/chat-send-flow.ts +++ b/src/stores/chat/chat-send-flow.ts @@ -1,5 +1,6 @@ import { fromCallback, fromPromise } from "xstate"; +import { ExceptionHandler } from "@/core/errors"; import { MessageQueue } from "@/core/net/message-queue"; import type { ChatSendResponse } from "@/data/dto/chat"; import { getChatRepository } from "@/data/repositories/chat_repository"; @@ -131,8 +132,7 @@ function createMessageQueueActor( const output = await sendMessageViaHttp(content); sendBack({ type: "ChatQueuedHttpDone", output }); } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Message send failed"; + const errorMessage = ExceptionHandler.message(error); log.error("[chat-machine] message queue send failed", { error, }); diff --git a/src/stores/payment/payment-machine.helpers.ts b/src/stores/payment/payment-machine.helpers.ts index 0b8341ee..f69388c9 100644 --- a/src/stores/payment/payment-machine.helpers.ts +++ b/src/stores/payment/payment-machine.helpers.ts @@ -1,3 +1,4 @@ +import { ExceptionHandler } from "@/core/errors"; import { PaymentPlan, type PaymentPlansResponse } from "@/data/dto/payment"; import type { PaymentState } from "./payment-state"; @@ -7,7 +8,7 @@ export const PAYMENT_TIMEOUT_ERROR_MESSAGE = "Payment timed out. Please try again."; export function toPaymentErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); + return ExceptionHandler.message(error); } export function hasOrderPollingTimedOut( diff --git a/src/utils/__tests__/result.test.ts b/src/utils/__tests__/result.test.ts new file mode 100644 index 00000000..03906c40 --- /dev/null +++ b/src/utils/__tests__/result.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { AppException, ExceptionCode } from "@/core/errors"; + +import { Result, toError } from "../result"; + +describe("Result error normalization", () => { + it("normalizes unknown thrown values to AppException", async () => { + const result = await Result.wrap(async () => { + throw "boom"; + }); + + expect(Result.isErr(result)).toBe(true); + if (Result.isOk(result)) throw new Error("Expected Result.err"); + + expect(result.error).toBeInstanceOf(AppException); + expect(result.error.message).toBe("boom"); + expect((result.error as AppException).code).toBe(ExceptionCode.unknown); + }); + + it("returns AppException from toError", () => { + const error = toError(new TypeError("Failed to fetch")); + + expect(error).toBeInstanceOf(AppException); + expect(error.message).toBe("Network connection failed."); + expect((error as AppException).code).toBe(ExceptionCode.network); + }); +}); diff --git a/src/utils/result.ts b/src/utils/result.ts index 0420ae9f..95967c49 100644 --- a/src/utils/result.ts +++ b/src/utils/result.ts @@ -10,10 +10,13 @@ * 如需在同模块中混用,可用 `import { Result as StorageResult } from * "@/data/storage/result"` 别名。 * - * `error` 固定为 `Error`(非 `unknown`):HTTP 拦截器与共享 unwrap 都抛 `ApiError` - * (`ApiError extends Error`),仓库 `Result.wrap` 捕获后用 `toError` 规范化, - * 调用方能可靠地 `instanceof ApiError` / `error.message` 访问错误信息。 + * `error` 仍固定为 `Error`(非 `unknown`):仓库 `Result.wrap` 捕获后用 + * `ExceptionHandler` 统一归一化为 `AppException`,调用方可以稳定读取 + * `error.message`;需要结构化错误时可继续通过 `ExceptionHandler.handle(error)` + * 得到简化后的 `{ code, message }`。 */ +import { AppException, ExceptionHandler } from "@/core/errors"; + import { Logger } from "./logger"; export type Result = @@ -30,7 +33,7 @@ export const Result = { }, /** - * 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `Error`。 + * 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `AppException`。 */ err(error: unknown): Result { return { success: false, error: toError(error) }; @@ -56,11 +59,11 @@ export const Result = { * `*Api` 调用从「throw 风格」桥接到「Result 风格」。 * * 错误处理策略(重点): - * 1. 捕获后用 `toError` 规范化(保留 .stack / .code / .status 等字段) + * 1. 捕获后用 `toError` 规范化为 `AppException`,原始 thrown 值放入 `cause` * 2. 用 Logger 记录(带 stack + component=Result 标签)—— 调试 dev 控制台可见, * 生产环境走 JSON pipeline 接入日志平台 * 3. 不重新抛出 —— 调用方拿到的永远是 `Result`(throw 风格调用方可在 - * 最外层显式 `throw Result.unwrap(r)`) + * 最外层显式 `throw r.error`) */ async wrap(thunk: () => Promise): Promise> { try { @@ -77,17 +80,11 @@ export const Result = { } as const; /** - * 把任意 thrown 值规范化为 `Error`: - * - 已是 `Error` 子类 → 原样返回(保留 `.code` / `.status` 等自定义字段) - * - string → `new Error(str)` - * - 其他 → `new Error(JSON.stringify(value))`,失败则 `new Error(String(value))` + * 把任意 thrown 值规范化为 `AppException`: + * - 已是 `AppException` → 原样返回 + * - 其他错误 → 交给 `ExceptionHandler` 收敛为 `{ code, message }` + * - 原始值会保存在 `cause`,方便日志和调试继续追溯 */ -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)); - } +export function toError(value: unknown): AppException { + return ExceptionHandler.toError(value); } diff --git a/src/utils/session-storage.ts b/src/utils/session-storage.ts index 364ddc88..c81d92c8 100644 --- a/src/utils/session-storage.ts +++ b/src/utils/session-storage.ts @@ -36,7 +36,7 @@ export class SessionAsyncUtil { const json = typeof value === "string" ? JSON.parse(value) : value; return Result.ok(schema.parse(json)); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -50,7 +50,7 @@ export class SessionAsyncUtil { await SessionAsyncUtil._storage.setItem(key, JSON.stringify(validated)); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -59,15 +59,11 @@ export class SessionAsyncUtil { await SessionAsyncUtil._storage.removeItem(key); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } } -function toError(e: unknown): Error { - return e instanceof Error ? e : new Error(String(e)); -} - function createDefaultSessionStorage(): Storage { if (typeof window === "undefined") { return createStorage({ driver: memoryDriver() }); diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 92498a2d..9de399f3 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -50,7 +50,7 @@ export class SpAsyncUtil { if (value.length === 0) return Result.ok(null); return Result.ok(value); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -62,7 +62,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, value); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -74,7 +74,7 @@ export class SpAsyncUtil { if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null); return Result.ok(Math.trunc(value)); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -83,7 +83,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, value); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -95,7 +95,7 @@ export class SpAsyncUtil { if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null); return Result.ok(value); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -104,7 +104,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, value); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -116,7 +116,7 @@ export class SpAsyncUtil { if (typeof value !== "boolean") return Result.ok(null); return Result.ok(value); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -125,7 +125,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, value); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -138,7 +138,7 @@ export class SpAsyncUtil { if (!value.every((v) => typeof v === "string")) return Result.ok(null); return Result.ok(value); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -150,7 +150,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, value); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -166,7 +166,7 @@ export class SpAsyncUtil { const parsed = schema.parse(json); return Result.ok(parsed); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -180,7 +180,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.setItem(key, JSON.stringify(validated)); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -190,7 +190,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.removeItem(key); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -199,7 +199,7 @@ export class SpAsyncUtil { await SpAsyncUtil._storage.clear(); return Result.ok(undefined); } catch (e) { - return Result.err(toError(e)); + return Result.err(e); } } @@ -208,18 +208,11 @@ export class SpAsyncUtil { const exists = await SpAsyncUtil._storage.hasItem(key); return Result.ok(exists); } catch (e) { - return Result.err(toError(e)); + return Result.err(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() });