Files
cozsweet-frontend-nextjs/src/utils/result.ts
T

99 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 仓库和持久化操作共享的 Result<T>。
*
* `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<T> =
| { readonly success: true; readonly data: T }
| { readonly success: false; readonly error: Error };
/** Result.wrap 专用 logger —— 自动带 component: "Result" 标签 */
const log = new Logger("Result");
type ResultErrorLogLevel = "info" | "error";
export interface ResultWrapOptions {
errorLogLevel?:
| ResultErrorLogLevel
| ((error: Error) => ResultErrorLogLevel);
}
export const Result = {
/** 构造成功结果。 */
ok<T>(data: T): Result<T> {
return { success: true, data };
},
/**
* 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `AppException`。
*/
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 风格」。
*
* 错误处理策略(重点):
* 1. 捕获后用 `toError` 规范化为 `AppException`,原始 thrown 值放入 `cause`
* 2. 用 Logger 记录(带 stack + component=Result 标签)—— 调试 dev 控制台可见,
* 生产环境走 JSON pipeline 接入日志平台
* 3. 不重新抛出 —— 调用方拿到的永远是 `Result<T>`throw 风格调用方可在
* 最外层显式 `throw r.error`
*/
async wrap<T>(
thunk: () => Promise<T>,
options?: ResultWrapOptions,
): Promise<Result<T>> {
try {
return Result.ok(await thunk());
} catch (e) {
const err = toError(e);
const logLevel =
typeof options?.errorLogLevel === "function"
? options.errorLogLevel(err)
: options?.errorLogLevel ?? "error";
const details = { err, stack: err.stack, type: err.constructor.name };
if (logLevel === "info") {
log.info(details, "Result.wrap caught exception");
} else {
log.error(details, "Result.wrap caught exception");
}
return Result.err(err);
}
},
} as const;
/**
* 把任意 thrown 值规范化为 `AppException`
* - 已是 `AppException` → 原样返回
* - 其他错误 → 交给 `ExceptionHandler` 收敛为 `{ code, message }`
* - 原始值会保存在 `cause`,方便日志和调试继续追溯
*/
export function toError(value: unknown): AppException {
return ExceptionHandler.toError(value);
}