99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
/**
|
||
* 仓库和持久化操作共享的 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);
|
||
}
|