@mablock/status 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ # @mablock/status
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ 一个零运行时依赖的 TypeScript 库,提供 Abseil 风格的 `Status` 和 `StatusOr<T>` 错误处理。
6
+
7
+ 这个包遵循 Abseil 的核心约束:
8
+
9
+ - `Status` 表示成功或带消息、payload 的规范错误,不保存业务值。
10
+ - `StatusOr<T>` 只包含一个 `T` 类型的值或一个非 OK 的 `Status`。
11
+ - 不含值的 OK `Status` 不能转换成 `StatusOr<T>`。
12
+
13
+ 本项目受 Abseil 启发,但与 Google 无关联,也不由 Google 维护。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install @mablock/status
19
+ ```
20
+
21
+ 包内同时提供 ESM、CommonJS 和 TypeScript 类型声明。在 Node.js 中使用时需要 Node.js 18 或更高版本。
22
+
23
+ ## 基本用法:把错误逐层传到顶层
24
+
25
+ 推荐的基本流程是:
26
+
27
+ 1. 叶子函数成功时返回普通业务值 `T`,失败时返回 `failure(status)`。
28
+ 2. 每个中间层执行 `yield* ensureOk(ret)`。遇到失败时,当前层立即停止并把原始错误返回给调用方。
29
+ 3. 上层继续通过 `ensureOk` 传递错误,错误可以一级一级返回。
30
+ 4. 最顶层只需要调用一次 `hasError(ret)` 处理错误,不需要 `else`。
31
+
32
+ ```ts
33
+ import {
34
+ type StatusOr,
35
+ statusDo,
36
+ ensureOk,
37
+ hasError,
38
+ failure,
39
+ NotFoundError,
40
+ } from "@mablock/status";
41
+
42
+ interface User {
43
+ id: number;
44
+ name: string;
45
+ }
46
+
47
+ interface Profile {
48
+ user: User;
49
+ label: string;
50
+ }
51
+
52
+ function findUser(id: number): User {
53
+ if (id === 404)
54
+ return failure(NotFoundError(`User ${id} not found`));
55
+ return { id, name: "Ada" };
56
+ }
57
+
58
+ function loadProfile(id: number): StatusOr<Profile> {
59
+ return statusDo(function* () {
60
+ var ret = findUser(id);
61
+ yield* ensureOk(ret);
62
+
63
+ return {
64
+ user: ret,
65
+ label: `${ret.name}#${ret.id}`,
66
+ };
67
+ });
68
+ }
69
+
70
+ function prepareProfile(id: number): StatusOr<string> {
71
+ return statusDo(function* () {
72
+ var ret = loadProfile(id);
73
+ const profile = yield* ensureOk(ret);
74
+
75
+ return `Profile: ${profile.label}`;
76
+ });
77
+ }
78
+
79
+ var ret = prepareProfile(404);
80
+ if (hasError(ret)) {
81
+ console.error(ret.status().toString());
82
+ return;
83
+ }
84
+
85
+ console.log(ret.value());
86
+ ```
87
+
88
+ `ensureOk` 不会把失败结果变成业务值。在 `statusDo` 内部,它会中断当前 Generator,并保留原始错误状态。下一层可以再次使用 `ensureOk` 继续向上传递,直到最顶层通过 `hasError` 统一处理。
89
+
90
+ 如果顶层直接调用返回业务值的函数,写法更短:
91
+
92
+ ```ts
93
+ var ret = findUser(404);
94
+ if (hasError(ret)) {
95
+ console.error(ret.status().toString());
96
+ return;
97
+ }
98
+
99
+ console.log(ret.name);
100
+ ```
101
+
102
+ ## Status
103
+
104
+ 没有返回值的操作可以使用 `Status` 表示执行结果:
105
+
106
+ ```ts
107
+ import {
108
+ type Status,
109
+ OkStatus,
110
+ InvalidArgumentError,
111
+ } from "@mablock/status";
112
+
113
+ function validatePort(port: number): Status {
114
+ if (!Number.isInteger(port) || port < 1 || port > 65_535)
115
+ return InvalidArgumentError(`Invalid port: ${port}`);
116
+ return OkStatus();
117
+ }
118
+
119
+ const status = validatePort(8080);
120
+ if (!status.ok()) console.error(status.code(), status.message());
121
+ ```
122
+
123
+ 默认构造函数和 `OkStatus()` 都会创建 OK 状态。为 OK 状态传入消息时,消息会按照 Abseil 语义被丢弃。
124
+
125
+ ### 规范错误码
126
+
127
+ 以下 16 个错误构造函数对应 gRPC 和 Abseil 的规范错误码:
128
+
129
+ - `CancelledError(message?, payloads?)`
130
+ - `UnknownError(message?, payloads?)`
131
+ - `InvalidArgumentError(message?, payloads?)`
132
+ - `DeadlineExceededError(message?, payloads?)`
133
+ - `NotFoundError(message?, payloads?)`
134
+ - `AlreadyExistsError(message?, payloads?)`
135
+ - `PermissionDeniedError(message?, payloads?)`
136
+ - `ResourceExhaustedError(message?, payloads?)`
137
+ - `FailedPreconditionError(message?, payloads?)`
138
+ - `AbortedError(message?, payloads?)`
139
+ - `OutOfRangeError(message?, payloads?)`
140
+ - `UnimplementedError(message?, payloads?)`
141
+ - `InternalError(message?, payloads?)`
142
+ - `UnavailableError(message?, payloads?)`
143
+ - `DataLossError(message?, payloads?)`
144
+ - `UnauthenticatedError(message?, payloads?)`
145
+
146
+ ### Payload
147
+
148
+ `Status` 可以保存以 URI 或 type URL 为键的字符串或二进制 `Uint8Array` payload:
149
+
150
+ ```ts
151
+ import { NotFoundError } from "@mablock/status";
152
+
153
+ const status = NotFoundError("User not found");
154
+ status.setPayload(
155
+ "type.googleapis.com/google.rpc.ErrorInfo",
156
+ "USER_DISABLED",
157
+ );
158
+
159
+ if (status.hasPayload("type.googleapis.com/google.rpc.ErrorInfo")) {
160
+ const payload = status.getPayload(
161
+ "type.googleapis.com/google.rpc.ErrorInfo",
162
+ );
163
+ console.log(payload);
164
+ }
165
+ ```
166
+
167
+ ### 更新 Status
168
+
169
+ `status.update(other)` 保留遇到的第一个非 OK 状态,与 `absl::Status::Update` 的行为一致:
170
+
171
+ ```ts
172
+ import {
173
+ OkStatus,
174
+ NotFoundError,
175
+ CancelledError,
176
+ } from "@mablock/status";
177
+
178
+ const status = OkStatus();
179
+ status.update(NotFoundError("First error"));
180
+ status.update(CancelledError("Second error"));
181
+
182
+ console.log(status.code()); // StatusCode.NOT_FOUND
183
+ ```
184
+
185
+ ## StatusOr<T>
186
+
187
+ 需要在成功时返回值的操作可以使用 `StatusOr<T>`:
188
+
189
+ ```ts
190
+ import {
191
+ StatusOr,
192
+ statusOr,
193
+ statusOrError,
194
+ NotFoundError,
195
+ } from "@mablock/status";
196
+
197
+ interface User {
198
+ id: number;
199
+ name: string;
200
+ }
201
+
202
+ function findUser(id: number): StatusOr<User> {
203
+ if (id === 404)
204
+ return statusOrError(NotFoundError(`User ${id} was not found`));
205
+ return statusOr({ id, name: "Ada" });
206
+ }
207
+
208
+ const result = findUser(42);
209
+ if (result.ok()) {
210
+ console.log(result.value().name);
211
+ } else {
212
+ console.error(result.status().toString());
213
+ }
214
+ ```
215
+
216
+ C++ 可以从错误 `Status` 隐式构造 `StatusOr<T>`,TypeScript 没有对应的隐式转换。请使用 `statusOrError(error)` 或 `StatusOr.fromStatus(error)`。
217
+
218
+ `StatusOr.fromStatus(OkStatus())` 会抛出 `RangeError`,因为成功结果必须包含一个值。
219
+
220
+ ### 转换结果
221
+
222
+ ```ts
223
+ const displayName = findUser(42)
224
+ .transform((user) => user.name)
225
+ .valueOr("unknown");
226
+
227
+ const message = findUser(404).match({
228
+ ok: (user) => `Found ${user.name}`,
229
+ err: (status) => `Failed: ${status}`,
230
+ });
231
+ ```
232
+
233
+ 可用方法包括:
234
+
235
+ - `value()`:返回内部值;非 OK 时抛出 `StatusError`。
236
+ - `valueOr(defaultValue)`:返回内部值或指定的默认值。
237
+ - `valueOrCompute(fn)`:根据失败的 `Status` 计算默认值。
238
+ - `transform(fn)`:成功时将 `T` 映射为 `U`。
239
+ - `andThen(fn)`:成功时将 `T` 映射为 `StatusOr<U>`。
240
+ - `match({ ok, err })`:分别处理成功和失败结果。
241
+ - `StatusOr.all([a, b, c])`:将多个结果合并成一个元组结果。
242
+
243
+ ## Generator 流水线
244
+
245
+ `statusDo` 和 `statusDoAsync` 使用标准 Generator 提供类似 `ASSIGN_OR_RETURN` 的控制流:
246
+
247
+ ```ts
248
+ import {
249
+ type StatusOr,
250
+ statusDo,
251
+ statusOr,
252
+ statusOrError,
253
+ InvalidArgumentError,
254
+ ensureOk,
255
+ } from "@mablock/status";
256
+
257
+ function parseNumber(input: string): StatusOr<number> {
258
+ const value = Number(input);
259
+ if (Number.isNaN(value))
260
+ return statusOrError(
261
+ InvalidArgumentError(`Invalid number: ${input}`),
262
+ );
263
+ return statusOr(value);
264
+ }
265
+
266
+ const result = statusDo(function* () {
267
+ const left = yield* ensureOk(parseNumber("20"));
268
+
269
+ const rightOr = parseNumber("22");
270
+ yield* ensureOk(rightOr);
271
+ const right = rightOr.value();
272
+
273
+ return left + right;
274
+ });
275
+ ```
276
+
277
+ 流水线会在第一个错误处停止并关闭 Generator,因此 `finally` 清理逻辑仍会执行。
278
+
279
+ ## Failure sentinel
280
+
281
+ `failure(status)` 创建一个携带原始非 OK 状态的 Proxy,同时让函数的成功返回类型保持为 `T`。如果没有检查就读取失败 Proxy 的业务属性,会抛出 `StatusError`。中间层使用 `ensureOk`,顶层边界使用 `hasError`。
282
+
283
+ ### Proxy 属性访问
284
+
285
+ 使用 `asProxy` 包装 `StatusOr<T>` 后,可以直接访问 `T` 中与 `StatusOr` 不冲突的属性:
286
+
287
+ ```ts
288
+ import { asProxy, statusOr } from "@mablock/status";
289
+
290
+ const proxy = asProxy(statusOr({ id: 1, name: "Ada" }));
291
+ console.log(proxy.name); // "Ada"
292
+ console.log(proxy.ok()); // true
293
+ ```
294
+
295
+ ## JavaScript 错误转换
296
+
297
+ 原生 JavaScript 异常可以转换为规范 `Status`:
298
+
299
+ ```ts
300
+ import { fromError, tryCatch, tryCatchAsync } from "@mablock/status";
301
+
302
+ const status = fromError(new TypeError("Invalid argument"));
303
+ console.log(status.code()); // StatusCode.INVALID_ARGUMENT
304
+
305
+ const syncResult = tryCatch(() => JSON.parse(rawText));
306
+
307
+ const asyncResult = await tryCatchAsync(async () => fetchUserData());
308
+ ```
309
+
310
+ ## 状态判断
311
+
312
+ 包内提供用于判断具体错误状态的类型守卫:
313
+
314
+ ```ts
315
+ import {
316
+ isOk,
317
+ hasError,
318
+ isNotFound,
319
+ isInvalidArgument,
320
+ } from "@mablock/status";
321
+
322
+ if (isOk(result)) {
323
+ // result 已完成类型收窄
324
+ }
325
+
326
+ if (isNotFound(statusOr)) {
327
+ // 处理未找到错误
328
+ }
329
+ ```
330
+
331
+ ## 许可证
332
+
333
+ Apache-2.0