lakutata 3.0.0 → 3.1.0
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.
- package/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Lakutata 异常
|
|
2
|
+
|
|
3
|
+
`lakutata` 中的 `Exception` 是框架所有错误的基类,也是应用自己的异常的基类。异常带有一个供客户端和日志识别的错误码(`errno`)、一条由模板生成的消息,以及错误对应的 HTTP 状态码。动作失败时,传输方式把这些信息发给客户端。
|
|
4
|
+
|
|
5
|
+
## 1. 声明异常
|
|
6
|
+
|
|
7
|
+
继承 `Exception`,设置 `errno`;对于会发给客户端的错误,再设置 `statusCode`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import {Exception} from 'lakutata'
|
|
11
|
+
|
|
12
|
+
export class OrderNotFoundException extends Exception {
|
|
13
|
+
public errno: string = 'E_ORDER_NOT_FOUND'
|
|
14
|
+
|
|
15
|
+
public readonly statusCode: number = 404
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PaymentFailedException extends Exception {
|
|
19
|
+
public errno: string = 'E_PAYMENT_FAILED'
|
|
20
|
+
|
|
21
|
+
public readonly statusCode: number = 502
|
|
22
|
+
|
|
23
|
+
//自己的属性:随错误一起发送,也一起写入日志
|
|
24
|
+
public readonly gateway: string = 'stripe'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findOrder(id: number): never {
|
|
28
|
+
throw new OrderNotFoundException('Order {id} not found', {id: id})
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
异常带有这些属性:
|
|
33
|
+
|
|
34
|
+
| 属性 | 是什么 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `errno` | 错误码,可以是字符串(`'E_ORDER_NOT_FOUND'`)或数字 |
|
|
37
|
+
| `message`、`errMsg` | 消息;`errMsg` 是它的副本,会发给客户端 |
|
|
38
|
+
| `err` | 类名(`'OrderNotFoundException'`) |
|
|
39
|
+
| `statusCode` | 错误对应的 HTTP 状态码,默认 500 |
|
|
40
|
+
| `appId`、`appName` | 应用的 id 和名字;异常在应用加载选项之前创建时为 `'Unknown'` |
|
|
41
|
+
| `stack` | 调用栈 |
|
|
42
|
+
|
|
43
|
+
子类自己的属性,比如上面的 `gateway`,会和其他属性一起发送、一起写入日志。
|
|
44
|
+
|
|
45
|
+
## 2. 消息
|
|
46
|
+
|
|
47
|
+
构造函数的参数决定消息:
|
|
48
|
+
|
|
49
|
+
| 创建方式 | 消息 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `new OrderNotFoundException('Order {id} not found', {id: 7})` | `Order 7 not found` |
|
|
52
|
+
| `new OrderNotFoundException('Order {0} of {1} not found', 7, 'ada')` | `Order 7 of ada not found` |
|
|
53
|
+
| `new OrderNotFoundException('Order {0} of {1} not found', [7, 'ada'])` | `Order 7 of ada not found` |
|
|
54
|
+
| `new OrderNotFoundException('Order {0} not found', 7)` | `Order 7 not found` |
|
|
55
|
+
| `new OrderNotFoundException('Order %d not found (%s)', 7, 'archived')` | `Order 7 not found (archived)` |
|
|
56
|
+
| `new OrderNotFoundException('Order {id} of {owner} not found', {id: 7})` | `Order 7 of {owner} not found` |
|
|
57
|
+
| `new OrderNotFoundException('Order not found')` | `Order not found` |
|
|
58
|
+
| `new OrderNotFoundException(new Error('timeout'))` | `timeout` |
|
|
59
|
+
| `new OrderNotFoundException()` | `Order not found exception` |
|
|
60
|
+
|
|
61
|
+
- 模板从一个对象中取值(`{id}`),或者按位置从它后面的参数中取值(`{0}`、`{1}`),参数可以逐个给出,也可以放在一个数组里。`util.format()` 的占位符(`%s`、`%d`、`%j`……)使用同样的参数。
|
|
62
|
+
- 缺少的值会让占位符保留在消息中。生成消息从不抛出错误。
|
|
63
|
+
- 不给参数时,消息由类名中的单词组成,缩写保持大写(`HTTPRequestException` 得到 `HTTP request exception`)。
|
|
64
|
+
|
|
65
|
+
## 3. 默认消息与原因
|
|
66
|
+
|
|
67
|
+
要给异常一个默认消息,就在构造函数中接收它,再传给 `super()`:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import {Exception} from 'lakutata'
|
|
71
|
+
|
|
72
|
+
export class QuotaExceededException extends Exception {
|
|
73
|
+
public errno: string = 'E_QUOTA_EXCEEDED'
|
|
74
|
+
|
|
75
|
+
public readonly statusCode: number = 429
|
|
76
|
+
|
|
77
|
+
constructor(message: string = 'The quota of the month is used up') {
|
|
78
|
+
super(message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
不要在字段中设置 `message`:这个字段会替换抛出异常时给出的消息;并且在编译选项 `useDefineForClassFields` 为 `true` 时,`errMsg` 会保留默认消息。`lakutata` 发布的 `tsconfig.json` 把这个选项设为 `false`。
|
|
84
|
+
|
|
85
|
+
`new XxxException(error)` 只保留另一个错误的消息。要保留这个错误本身,把它设为 `cause`:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import {Exception} from 'lakutata'
|
|
89
|
+
|
|
90
|
+
class PaymentFailedException extends Exception {
|
|
91
|
+
public errno: string = 'E_PAYMENT_FAILED'
|
|
92
|
+
|
|
93
|
+
public readonly statusCode: number = 502
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function pay(orderId: number, charge: () => Promise<void>): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
await charge()
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const failure: PaymentFailedException = new PaymentFailedException('Payment of order {id} failed', {id: orderId})
|
|
101
|
+
failure.cause = error
|
|
102
|
+
throw failure
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
日志会在错误之后写出它的原因(见 [Logger](Logger.md) 第 2 节);HTTP 适配器会沿着原因查找状态码,第一个客户端错误(4xx)优先(见 [HTTP](HTTP.md) 第 6 节)。
|
|
108
|
+
|
|
109
|
+
## 4. 客户端收到什么
|
|
110
|
+
|
|
111
|
+
动作失败时,传输方式这样报告异常:
|
|
112
|
+
|
|
113
|
+
| 传输方式 | 客户端收到 |
|
|
114
|
+
|---|---|
|
|
115
|
+
| HTTP | 状态码 `statusCode` 和 `{errno, errMsg}`(见 [HTTP](HTTP.md) 第 6 节) |
|
|
116
|
+
| Socket.IO、Electron、HTTP 服务、NATS、gRPC | 完整的错误:它所有的自身属性(见 [ServiceCalls](ServiceCalls.md) 第 3 节) |
|
|
117
|
+
| JSON-RPC | 一个错误:输入不合法时错误码为 `-32602`,没有匹配的动作时为 `-32601`,应用正在停止时为 `-32001`,其他情况为 `-32000`;完整的错误放在 `data` 中 |
|
|
118
|
+
| 命令行 | 标准错误中的 `error: <消息>`,退出码为 1(见 [CLI](CLI.md)) |
|
|
119
|
+
| 定时任务 | 没有客户端:异常交给 `onError`,或者写入日志(见 [Schedules](Schedules.md)) |
|
|
120
|
+
|
|
121
|
+
- 选择 `statusCode` 时看错误出在哪一方:请求有误时用 400 到 499(资源不存在用 404,冲突用 409),应用自身出错时用 500 及以上。
|
|
122
|
+
- 被 DTO 拒绝的输入是 `InvalidValueException`,它的状态码是 400,表示客户端的错误。
|
|
123
|
+
|
|
124
|
+
## 5. 翻译与识别异常
|
|
125
|
+
|
|
126
|
+
使用 `I18nMiddleware` 时,异常的消息会按请求的语言翻译,翻译的键是 `errors.<errno>`,参数来自异常的模板(见 [I18n](I18n.md) 中的“异常消息的翻译”一节)。在请求之外,`lakutata` 中的 `exceptionParams(exception)` 返回这些参数。
|
|
127
|
+
|
|
128
|
+
在应用内部用 `instanceof` 识别异常,跨进程时用 `errno`:客户端收到的是错误的属性,而不是它的类。HTTP 服务、NATS 和 gRPC 的代理抛出的 `ServiceInvokeException` 带有远端错误的 `errno`。
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {Exception} from 'lakutata'
|
|
132
|
+
|
|
133
|
+
class OrderNotFoundException extends Exception {
|
|
134
|
+
public errno: string = 'E_ORDER_NOT_FOUND'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isMissingOrder(error: unknown): boolean {
|
|
138
|
+
return error instanceof OrderNotFoundException || (error as { errno?: unknown })?.errno === 'E_ORDER_NOT_FOUND'
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 6. 框架的异常
|
|
143
|
+
|
|
144
|
+
框架会抛出自己的异常。最常遇到的有:
|
|
145
|
+
|
|
146
|
+
| 异常 | `errno` | 什么时候 |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `InvalidValueException` | `E_INVALID_VALUE` | DTO 拒绝了一个值:动作的输入,或者对 `validate()` 的调用(400) |
|
|
149
|
+
| `InvalidObjectOptionsException` | `E_INVALID_OBJECT_OPTIONS` | 应用启动时,某个对象的选项无效 |
|
|
150
|
+
| `DependencyInjectionException` | `E_DEPENDENCY_INJECTION` | 无法创建对象:缺少某个依赖 |
|
|
151
|
+
| `AccessDeniedException` | `E_ACCESS_DENY` | 访问控制拒绝了请求(403) |
|
|
152
|
+
| `RouteNotFoundException` | `E_ROUTE_NOT_FOUND` | 没有 HTTP 动作对应这个路由和方法(404) |
|
|
153
|
+
| `ControllerActionNotFoundException` | `E_CONTROLLER_ACTION_NOT_FOUND` | 没有动作与服务消息或命令匹配(404) |
|
|
154
|
+
| `EntrypointStoppedException` | `E_ENTRYPOINT_STOPPED` | 应用正在停止(503) |
|
|
155
|
+
| `ServiceInvokeException` | `E_SERVICE_INVOKE` | 通过代理(HTTP 服务、NATS、gRPC)调用的动作失败;带有远端错误的各个属性 |
|
|
156
|
+
| `ServiceUnavailableException` | `E_SERVICE_UNAVAILABLE` | `HTTPServiceProxy` 调用的服务没有给出响应(503) |
|
|
157
|
+
|
|
158
|
+
每个包的 README 列出了它自己的异常。没有被任何代码捕获的异常,以及导致应用启动失败的异常,会交给应用的处理函数(`onUncaughtException`、`onFatalException`,见 [Application](Application.md) 第 2 节)。
|
package/doc/zh/GRPC.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Lakutata gRPC
|
|
2
|
+
|
|
3
|
+
`@lakutata/grpc` 包让应用通过 gRPC 提供服务动作,也能从另一个应用调用这些动作;任何有 gRPC 库的语言都可以调用它们。它用 TypeScript 基于 Node.js(`^22.13.0 || >=24.11.0`)的 HTTP/2 模块实现,不需要原生插件,也不需要生成代码。`lakutata` 不包含这个包,需要另外安装:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install lakutata @lakutata/grpc
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 1. 提供服务动作
|
|
10
|
+
|
|
11
|
+
`@lakutata/grpc/service` 中的 `grpcServiceEntrypoint()` 是入口的一种 `service` 适配器。控制器里的 `@ServiceAction` 方法可以通过 gRPC 按模式调用,和其他服务传输方式一样。
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {Application, Exception} from 'lakutata'
|
|
15
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
16
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
17
|
+
import {grpcServiceEntrypoint} from '@lakutata/grpc/service'
|
|
18
|
+
|
|
19
|
+
class OrderNotFoundException extends Exception {
|
|
20
|
+
public errno: string = 'E_ORDER_NOT_FOUND'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class OrderController extends Controller {
|
|
24
|
+
//用 {cmd: 'order.get', id: 1} 调用
|
|
25
|
+
@ServiceAction({cmd: 'order.get'})
|
|
26
|
+
public async get(input: { id: number }): Promise<{ id: number, status: string }> {
|
|
27
|
+
if (input.id === 0) throw new OrderNotFoundException('Order {id} not found', {id: input.id})
|
|
28
|
+
return {id: input.id, status: 'paid'}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Application.run(() => ({
|
|
33
|
+
id: 'orders.service',
|
|
34
|
+
name: 'Orders',
|
|
35
|
+
components: {
|
|
36
|
+
entrypoint: buildEntrypoints({
|
|
37
|
+
controllers: [OrderController],
|
|
38
|
+
service: grpcServiceEntrypoint({port: 50051})
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
bootstrap: ['entrypoint']
|
|
42
|
+
}))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`grpcServiceEntrypoint` 的选项:
|
|
46
|
+
|
|
47
|
+
| 选项 | 默认值 | 含义 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `port` | `50051` | 端口;`0` 表示随机选一个空闲端口 |
|
|
50
|
+
| `host` | 所有网络接口 | 监听的地址 |
|
|
51
|
+
| `socketPath` | | 改用 unix socket,代替端口和地址 |
|
|
52
|
+
| `tls` | 无(明文 HTTP/2) | `{cert, key}` 启用 TLS;再加上 `ca` 时,客户端必须出示由它签发的证书(双向 TLS) |
|
|
53
|
+
| `maxMessageSize` | 4 MiB | 请求的最大大小(解压前后都算);超过时以 `RESOURCE_EXHAUSTED` 失败 |
|
|
54
|
+
| `compression` | `'identity'` | 设为 `'gzip'` 或 `'deflate'` 时,对支持的客户端压缩响应。压缩过的请求总是可以接收 |
|
|
55
|
+
| `listening` | | `(address) => void`,服务开始监听后调用;启动过程会等它完成 |
|
|
56
|
+
| `middlewares` | | 通过 gRPC 调用的动作专用的中间件,在入口的中间件之后执行 |
|
|
57
|
+
| `name` | | 适配器的名字,用来区分多个适配器 |
|
|
58
|
+
|
|
59
|
+
服务端不提供反射服务,也不提供健康检查服务。
|
|
60
|
+
|
|
61
|
+
## 2. 从另一个应用调用
|
|
62
|
+
|
|
63
|
+
`GRPCServiceProxy` 用来调用远端应用的动作。它是一个组件:用 `buildGRPCServiceProxy` 声明在 `components` 下,每个远端应用一个。
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import {Application} from 'lakutata'
|
|
67
|
+
import {ServiceInvokeException} from 'lakutata/com/entrypoint'
|
|
68
|
+
import {GRPCError, GRPCStatus} from '@lakutata/grpc'
|
|
69
|
+
import {buildGRPCServiceProxy, type GRPCServiceProxy} from '@lakutata/grpc/service'
|
|
70
|
+
|
|
71
|
+
Application
|
|
72
|
+
.run(() => ({
|
|
73
|
+
id: 'shop.app',
|
|
74
|
+
name: 'Shop',
|
|
75
|
+
components: {
|
|
76
|
+
orders: buildGRPCServiceProxy({address: 'orders.internal:50051'})
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
80
|
+
const orders: GRPCServiceProxy = await app.getObject<GRPCServiceProxy>('orders')
|
|
81
|
+
try {
|
|
82
|
+
//模式和输入放在同一个对象里,截止时间 5 秒
|
|
83
|
+
console.log(await orders.invoke({cmd: 'order.get', id: 1}, 5000))
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof ServiceInvokeException) {
|
|
86
|
+
//动作失败:它的 errno、消息和其他属性
|
|
87
|
+
console.log(error.errno, error.message)
|
|
88
|
+
} else if (error instanceof GRPCError && error.code === GRPCStatus.UNAVAILABLE) {
|
|
89
|
+
console.log('The orders service is unreachable')
|
|
90
|
+
} else {
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`invoke(input, timeout)` 返回动作的结果(动作没有返回值时为 `null`)。timeout 默认 10 分钟,就是这次 gRPC 调用的截止时间:时间一到,调用失败,服务端的动作也会被中止(触发它的 `this.signal`)。
|
|
98
|
+
|
|
99
|
+
`buildGRPCServiceProxy` 的选项:
|
|
100
|
+
|
|
101
|
+
| 选项 | 默认值 | 含义 |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `address` | 必填 | `host:port`、`tcp://host:port`,或 unix socket:`/path`、`unix:///path` |
|
|
104
|
+
| `tls` | 无 | `{ca, cert, key, servername}`,都可省略。只要设置了(哪怕是 `{}`)就启用 TLS;`cert` 和 `key` 是双向 TLS 中客户端的证书 |
|
|
105
|
+
| `clientId` | 随机 UUID | 这个客户端的 id,随每次调用发送(见第 3 节) |
|
|
106
|
+
| `compression` | `'identity'` | 设为 `'gzip'` 或 `'deflate'` 时压缩请求 |
|
|
107
|
+
| `maxMessageSize` | 不限 | 接收的响应的最大大小 |
|
|
108
|
+
| `reconnectDelay` | `1000` | 事件流结束后,等待多少毫秒再重新打开(见第 4 节) |
|
|
109
|
+
|
|
110
|
+
代理在第一次调用时才建立连接,调用失败时不会自己重试。
|
|
111
|
+
|
|
112
|
+
## 3. 客户端 id、元数据与截止时间
|
|
113
|
+
|
|
114
|
+
每次调用都带着元数据 `id`,即客户端的 id:代理的 `clientId`,或者其他语言的客户端发送的 `id` 头。动作通过 `this.context.clientId` 读取它。
|
|
115
|
+
|
|
116
|
+
`grpcCallOfContext(context)` 让动作拿到整个 gRPC 调用:客户端发送的元数据、截止时间和对端地址。请求来自其他传输方式时,它返回 `undefined`。
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import {Controller, type ServiceContext} from 'lakutata/com/entrypoint'
|
|
120
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
121
|
+
import {grpcCallOfContext} from '@lakutata/grpc/service'
|
|
122
|
+
import type {GRPCServerCall} from '@lakutata/grpc'
|
|
123
|
+
|
|
124
|
+
class OrderController extends Controller {
|
|
125
|
+
@ServiceAction({cmd: 'order.get'})
|
|
126
|
+
public async get(input: { id: number }): Promise<{ id: number, tenant: string }> {
|
|
127
|
+
const call: GRPCServerCall | undefined = grpcCallOfContext(this.context as ServiceContext)
|
|
128
|
+
//客户端发送的元数据:grpcurl -H 'tenant: acme' ...
|
|
129
|
+
const tenant: string = call?.metadata('tenant')[0] ?? 'default'
|
|
130
|
+
return {id: input.id, tenant: tenant}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `GRPCServiceProxy` 只发送 `id` 这一项元数据。需要其他元数据(比如授权令牌)的客户端,可以用 `GRPCConnection`(见第 8 节)或其他语言的客户端。
|
|
136
|
+
- 同一个元数据键的值会按逗号拆分,所以 id 里不要有逗号。
|
|
137
|
+
- 客户端取消调用或截止时间已过时,动作的 `this.signal` 会被触发,结果会被丢弃。耗时长的动作应该检查这个信号。
|
|
138
|
+
|
|
139
|
+
## 4. 事件
|
|
140
|
+
|
|
141
|
+
动作可以用 `entrypoint.notify(clientId, event, ...args)` 向某个客户端推送事件,或者用 `entrypoint.broadcast(event, ...args)` 推送给所有已连接的客户端。`notify` 返回客户端是否收到了事件,`broadcast` 返回客户端的数量。
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import {Controller, type Entrypoint, type ServiceContext} from 'lakutata/com/entrypoint'
|
|
145
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
146
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
147
|
+
|
|
148
|
+
class OrderController extends Controller {
|
|
149
|
+
@Inject('entrypoint')
|
|
150
|
+
protected readonly entrypoint: Entrypoint
|
|
151
|
+
|
|
152
|
+
@ServiceAction({cmd: 'order.ship'})
|
|
153
|
+
public async ship(input: { id: number }): Promise<void> {
|
|
154
|
+
const clientId: string | undefined = (this.context as ServiceContext).clientId
|
|
155
|
+
if (clientId) this.entrypoint.notify(clientId, 'orderChanged', {id: input.id, status: 'shipped'})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
另一端用 `orders.on('orderChanged', listener)` 监听;`once` 和 `off` 的用法与事件发射器相同。代理在注册第一个监听器时打开事件流,最后一个监听器移除时关闭。事件流意外结束时(比如服务重启了),会在 `reconnectDelay` 之后重新打开。
|
|
161
|
+
|
|
162
|
+
事件不会被保存:发送时没有连接的客户端会错过它。
|
|
163
|
+
|
|
164
|
+
## 5. 错误
|
|
165
|
+
|
|
166
|
+
动作失败不会让 gRPC 调用失败:调用本身是成功的,响应里带着这个错误,包括它的消息和全部属性(`errno`、`statusCode`,以及调用栈)。`GRPCServiceProxy` 把它作为 `lakutata/com/entrypoint` 中的 `ServiceInvokeException` 抛出:消息是远端的消息,`errno` 等属性从远端错误复制而来,`service` 是代理的地址。
|
|
167
|
+
|
|
168
|
+
gRPC 层面失败的调用会抛出 `@lakutata/grpc` 中的 `GRPCError`,它的 `code` 是一个 `GRPCStatus`:
|
|
169
|
+
|
|
170
|
+
| 状态 | 什么时候 |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `UNAVAILABLE` | 连不上服务 |
|
|
173
|
+
| `DEADLINE_EXCEEDED` | 超过截止时间 |
|
|
174
|
+
| `CANCELLED` | 调用被取消 |
|
|
175
|
+
| `RESOURCE_EXHAUSTED` | 消息超过 `maxMessageSize` |
|
|
176
|
+
| `UNIMPLEMENTED` | 方法不存在,或使用了服务端不支持的压缩方式 |
|
|
177
|
+
|
|
178
|
+
没有模式匹配的调用会以 errno `E_CONTROLLER_ACTION_NOT_FOUND` 失败,和其他服务传输方式一样。
|
|
179
|
+
|
|
180
|
+
## 6. 从其他语言调用
|
|
181
|
+
|
|
182
|
+
`SERVICE_PROTO` 是这个服务的 `.proto` 文件,用它生成其他语言的客户端:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import {writeFileSync} from 'node:fs'
|
|
186
|
+
import {SERVICE_PROTO} from '@lakutata/grpc/service'
|
|
187
|
+
|
|
188
|
+
writeFileSync('service.proto', SERVICE_PROTO)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
service Service { // package lakutata.service.v1
|
|
193
|
+
rpc Invoke (InvokeRequest) returns (InvokeResponse); // data:模式和输入的 JSON
|
|
194
|
+
rpc Events (EventsRequest) returns (stream Event); // 推送给该客户端的事件,第一个是 "client"(它的 id)
|
|
195
|
+
}
|
|
196
|
+
message InvokeResponse { bool success = 1; bytes payload = 2; bytes error = 3; } // payload、error:JSON
|
|
197
|
+
message Event { string event = 1; bytes args = 2; } // args:JSON 数组
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
调用时,在 `InvokeRequest.data` 里放模式和输入的 JSON,在元数据 `id` 里放客户端的 id。响应的 `payload` 是结果的 JSON,或者 `error` 是错误的 JSON。用 grpcurl 调用(它把 `bytes` 字段显示为 base64):
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
DATA=$(printf '{"cmd":"order.get","id":7}' | base64)
|
|
204
|
+
grpcurl -plaintext -proto service.proto -H 'id: go-1' -d "{\"data\": \"$DATA\"}" localhost:50051 lakutata.service.v1.Service/Invoke
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
用 Python(`grpcio` 包,存根由 `grpcio-tools` 生成):
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
import json
|
|
211
|
+
import grpc
|
|
212
|
+
import service_pb2 as pb
|
|
213
|
+
import service_pb2_grpc as rpc
|
|
214
|
+
|
|
215
|
+
stub = rpc.ServiceStub(grpc.insecure_channel('localhost:50051'))
|
|
216
|
+
response = stub.Invoke(pb.InvokeRequest(data=json.dumps({'cmd': 'order.get', 'id': 5}).encode()), metadata=[('id', 'python-1')], timeout=5)
|
|
217
|
+
print(json.loads(response.payload) if response.success else json.loads(response.error))
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 7. 停止
|
|
221
|
+
|
|
222
|
+
应用停止时,服务会拒绝新的调用:代理把它作为 `ServiceInvokeException` 抛出,其 `errno` 为 `E_ENTRYPOINT_STOPPED`。正在进行的调用有入口的 `shutdownTimeout`(默认 30 秒)可以完成。之后事件流结束,服务端关闭。
|
|
223
|
+
|
|
224
|
+
## 8. 底层组件
|
|
225
|
+
|
|
226
|
+
这个包也提供服务所依赖的底层组件,脱离 lakutata 也能使用:`GRPCConnection` 是可以调用任何 gRPC 服务端的客户端(一元调用、服务端流、双向流、元数据、TLS);`GRPCServer` 是服务端;`encode`/`decode` 是基于 schema 的 protobuf 编解码器。详见 `@lakutata/grpc` 的 README。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# 快速开始
|
|
2
|
+
|
|
3
|
+
本页先用模板创建第一个 Lakutata 项目,然后介绍如何在已有项目中加入框架,并手写一个应用。
|
|
4
|
+
|
|
5
|
+
## 1. 环境要求
|
|
6
|
+
|
|
7
|
+
- Node.js `^22.13.0 || >=24.11.0`:22.13 及以后的 22.x,或 24.11 及以后的版本(不支持 23)。
|
|
8
|
+
- TypeScript,开启它的装饰器(`experimentalDecorators`)及其元数据(`emitDecoratorMetadata`)。框架所需的编译选项以 `lakutata/tsconfig.json` 发布;模板使用 TypeScript 6。
|
|
9
|
+
- ES 模块(`"type": "module"`),和模板一样;包里同时也提供了 CommonJS 构建。
|
|
10
|
+
|
|
11
|
+
## 2. 创建项目
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @lakutata/cli create --name my-app
|
|
15
|
+
cd my-app
|
|
16
|
+
npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`npm run dev` 编译项目并启动它;日志中会出现 `my-app started in development mode`,应用在 8080 端口上响应:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl 'http://localhost:8080/example?name=you'
|
|
23
|
+
# {"message":"Hello, you!"}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`default` 模板是一个小型 HTTP 应用。其他模板从另一类应用开始:
|
|
27
|
+
|
|
28
|
+
| 模板 | 应用 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `default` | 提供 HTTP 动作的最小应用 |
|
|
31
|
+
| `cli` | 命令行工具,构建为不需要 Node.js 就能运行的可执行文件 |
|
|
32
|
+
| `service` | 以 Docker 镜像部署的 HTTP API |
|
|
33
|
+
| `web-react`、`web-vue` | 提供 React 或 Vue 单页应用(Vite)的 HTTP API |
|
|
34
|
+
| `web-next`、`web-nuxt` | 在同一个进程中提供 Next.js 或 Nuxt 应用的 HTTP API |
|
|
35
|
+
| `electron-react`、`electron-vue` | 桌面应用:Electron,主进程通过 IPC 为 React 或 Vue 渲染进程提供服务 |
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @lakutata/cli create --name shop --template service --with i18n,installer
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`--with` 加入可选功能:所有模板都有 `i18n`(多语言消息),`cli`、`service` 和 `web-*` 模板还有 `installer`(安装包和系统服务)。在终端里没有给出 `--with` 时,工具会列出这些功能供选择。模板中的文件见 [ProjectStructure](ProjectStructure.md),`lakutata create` 的选项见 [Command](Command.md)。
|
|
42
|
+
|
|
43
|
+
`default` 模板的脚本:
|
|
44
|
+
|
|
45
|
+
| 脚本 | 作用 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `npm run dev` | 把项目编译到 `build/` 并启动 |
|
|
48
|
+
| `npm run build` | 把项目编译到 `build/` |
|
|
49
|
+
| `npm start` | 启动编译好的应用 |
|
|
50
|
+
| `npm test` | 编译项目并运行测试(`node --test`) |
|
|
51
|
+
|
|
52
|
+
其他模板是包含多个包的工作区,它们的脚本作用于每个包,`npm test` 也会先构建各个包再运行测试(见 [ProjectStructure](ProjectStructure.md) 第 2 节)。
|
|
53
|
+
|
|
54
|
+
## 3. 在已有项目中加入 Lakutata
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install lakutata express
|
|
58
|
+
npm install --save-dev typescript @types/node @types/express @lakutata/cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`express` 是下面用到的 HTTP 适配器所基于的库。适配器和驱动都是可选依赖,用到时才安装:`fastify`,`commander` 和 `@inquirer/prompts`(命令行工具),`socket.io`,`ws`,数据库驱动(`pg`、`mysql2`、`better-sqlite3` 等),缓存存储(`redis` 等)。
|
|
62
|
+
|
|
63
|
+
`tsconfig.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"extends": "lakutata/tsconfig.json",
|
|
68
|
+
"compilerOptions": {
|
|
69
|
+
"rootDir": "src",
|
|
70
|
+
"outDir": "build"
|
|
71
|
+
},
|
|
72
|
+
"include": ["src"]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
请保留 `include`:发布的编译选项也会编译 JavaScript 文件(`allowJs`),没有它的话,项目根目录下的配置文件(`eslint.config.js`、`vite.config.ts`)会让构建失败。在 `package.json` 中设置 `"type": "module"`,并参照模板加上脚本:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"type": "module",
|
|
81
|
+
"main": "build/App.js",
|
|
82
|
+
"scripts": {
|
|
83
|
+
"clean": "lakutata clean",
|
|
84
|
+
"build": "npm run clean && tsc",
|
|
85
|
+
"start": "node build/App.js"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
源码中的相对导入以 `.js` 结尾(`import {config} from './config/Config.js'`),因为 Node.js 在编译后的代码中按这个路径解析。
|
|
91
|
+
|
|
92
|
+
## 4. 第一个应用
|
|
93
|
+
|
|
94
|
+
`src/App.ts`:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {Application, DTO, type ILogger} from 'lakutata'
|
|
98
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
99
|
+
import {GET, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
100
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
101
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
102
|
+
|
|
103
|
+
class GreetDTO extends DTO {
|
|
104
|
+
@Expect(DTO.String().optional().default('world'))
|
|
105
|
+
public name: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class GreetController extends Controller {
|
|
109
|
+
//GET /greet?name=you,以及服务动作 {cmd: 'greet', name: 'you'}
|
|
110
|
+
@GET('/greet', GreetDTO)
|
|
111
|
+
@ServiceAction({cmd: 'greet'}, GreetDTO)
|
|
112
|
+
public async greet(input: GreetDTO): Promise<{ message: string }> {
|
|
113
|
+
return {message: `Hello, ${input.name}!`}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Application
|
|
118
|
+
.run(() => ({
|
|
119
|
+
id: 'com.example.greeter',
|
|
120
|
+
name: 'Greeter',
|
|
121
|
+
components: {
|
|
122
|
+
entrypoint: buildEntrypoints({
|
|
123
|
+
controllers: [GreetController],
|
|
124
|
+
http: expressHTTPEntrypoint({port: 8080})
|
|
125
|
+
})
|
|
126
|
+
},
|
|
127
|
+
bootstrap: ['entrypoint']
|
|
128
|
+
}))
|
|
129
|
+
.onLaunched((app: Application, log: ILogger): void => log.info('%s started', app.appName))
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run build && npm start
|
|
134
|
+
curl 'http://localhost:8080/greet?name=you'
|
|
135
|
+
# {"message":"Hello, you!"}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `GreetDTO` 校验输入:`name` 可选,默认为 `world`([DTOValidation](DTOValidation.md))。
|
|
139
|
+
- `GreetController` 声明一个动作,这里通过 HTTP 提供;同一个方法也可以同时作为命令行工具的命令或服务动作([Controllers](Controllers.md))。
|
|
140
|
+
- `Application.run()` 声明应用的组件:`entrypoint` 组件通过它的适配器对外提供控制器,`bootstrap` 在启动时创建它([Application](Application.md)、[Entrypoint](Entrypoint.md))。
|
|
141
|
+
|
|
142
|
+
## 5. 接下来
|
|
143
|
+
|
|
144
|
+
- [示例](examples/README.md):一步步从模板构建完整的应用。
|
|
145
|
+
- [ProjectStructure](ProjectStructure.md):项目中的文件和脚本。
|
|
146
|
+
- [Application](Application.md)、[Objects](Objects.md) 和 [Injection](Injection.md):怎样声明应用,以及应用中的对象如何创建、如何获取彼此。
|
|
147
|
+
- [Controllers](Controllers.md) 和 [HTTP](HTTP.md):动作、它们的输入和响应。
|
|
148
|
+
- [Command](Command.md)、[Workspaces](Workspaces.md)、[NativeAddons](NativeAddons.md)、[Packaging](Packaging.md) 和 [Installers](Installers.md):`lakutata` 命令、项目中的各个包,以及打包后的应用。
|