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.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
@@ -1,408 +0,0 @@
1
- # Lakutata Controllers and Actions
2
-
3
- The `Entrypoint` component (`lakutata/com/entrypoint`, package `@lakutata/entrypoint`) serves the methods of the
4
- controllers, the **actions**, over several transports. An action is addressed by an HTTP route, a CLI command, a
5
- service pattern or a schedule, and one method can carry several of these addresses at once: its input is validated by
6
- its DTO, checked by the access control and passed through the middlewares the same way whatever the transport. The
7
- README of `@lakutata/entrypoint` and the typings of `lakutata/com/entrypoint` and `lakutata/decorator/ctrl` give the
8
- details.
9
-
10
- ## 1. Controllers
11
-
12
- A controller extends `Controller` (`lakutata/com/entrypoint`), a provider whose lifetime is `@Scoped` (locked): each
13
- request creates its own controller in the request's scope and destroys it when the request ends; the providers it
14
- injects are scoped to the request too.
15
-
16
- - `this.context` is the context of the request: an `HTTPContext` (its `request`, `response`, `route`, `method`), a
17
- `CLIContext` (its `command`), a `ServiceContext` (its `clientId`) or a `ScheduleContext`; tell them apart by their
18
- class or their `type`.
19
- - `this.signal` is an `AbortSignal` aborted when the request is: its client gone, its deadline exceeded, or the
20
- entrypoint stopping. Long-running actions should stop then: their result is dropped.
21
- - The method receives its input (the data of the request, validated by the DTO of the action, its defaults applied and
22
- its unknown keys removed) as a plain object, not a DTO instance. Without a DTO, the input is accepted as it is.
23
- - The value returned is the response; an exception is answered with its `statusCode` (HTTP) and its `errno` and
24
- message, an invalid input with the status 400.
25
-
26
- ```typescript
27
- import {Application, DTO} from 'lakutata'
28
- import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
29
- import {CLIAction, GET, POST, Schedule, ServiceAction} from 'lakutata/decorator/ctrl'
30
- import {Expect} from 'lakutata/decorator/dto'
31
- import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
32
- import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
33
- import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
34
-
35
- class GetOrderDTO extends DTO {
36
- //The route parameters and the query are strings: strict(false) converts them
37
- @Expect(DTO.Number().integer().strict(false).required())
38
- public id: number
39
- }
40
-
41
- class CreateOrderDTO extends DTO {
42
- @Expect(DTO.String().required())
43
- public product: string
44
-
45
- @Expect(DTO.Number().integer().min(1).default(1))
46
- public quantity: number
47
- }
48
-
49
- class OrderController extends Controller {
50
- //GET /orders/42, the command "order --id 42", the service message {cmd: 'order.get', id: 42}
51
- @GET('/orders/:id', GetOrderDTO)
52
- @CLIAction('order', GetOrderDTO)
53
- @ServiceAction({cmd: 'order.get'}, GetOrderDTO)
54
- public async get(input: GetOrderDTO): Promise<{ id: number }> {
55
- return {id: input.id}
56
- }
57
-
58
- //POST /orders with a JSON body
59
- @POST('/orders', CreateOrderDTO)
60
- public async create(input: CreateOrderDTO): Promise<{ product: string, quantity: number }> {
61
- return input
62
- }
63
-
64
- //Every 5 minutes
65
- @Schedule('5m')
66
- public async report(): Promise<void> {
67
- }
68
- }
69
-
70
- Application.run(() => ({
71
- id: 'orders.app',
72
- name: 'Orders',
73
- components: {
74
- entrypoint: buildEntrypoints({
75
- controllers: [OrderController],
76
- http: expressHTTPEntrypoint({port: 8080}),
77
- cli: commanderCLIEntrypoint(),
78
- service: socketIOServiceEntrypoint({port: 8081}),
79
- schedule: scheduleEntrypoint()
80
- })
81
- },
82
- bootstrap: ['entrypoint']
83
- }))
84
- ```
85
-
86
- ## 2. The action decorators (`lakutata/decorator/ctrl`)
87
-
88
- Each decorator takes its address, then optionally the DTO class of the input, then optionally the action options
89
- (`acl`, `name`, `description`, `groups`, `rule`, `middlewares`).
90
-
91
- ### HTTP: `@HTTPAction`, `@GET`, `@POST`, `@PUT`, `@PATCH`, `@DELETE`, `@HEAD`, `@OPTIONS`
92
-
93
- ```text
94
- @HTTPAction(route, method | methods, DTOClass?, options?)
95
- @GET(route, DTOClass?, options?)
96
- ```
97
-
98
- - The route uses the syntax of the HTTP adapter (`/users/:id`); the method is case-insensitive (`'get'`, `['get', 'post']`).
99
- - The input is the query merged with the parsed body (JSON or URL-encoded), then the route parameters over them. The
100
- query values and the route parameters are strings and the schemas are strict: convert them with `.strict(false)`.
101
- - The value returned is answered as JSON (204 for `undefined`). `Result.status(201, data, headers)`,
102
- `Result.file(path)` and `Result.events(producer)` (Server-Sent Events), from `lakutata/com/entrypoint`, control the
103
- response.
104
-
105
- ### CLI: `@CLIAction`
106
-
107
- ```text
108
- @CLIAction(command, DTOClass?, options?)
109
- ```
110
-
111
- - The command is its name, optionally followed by positional arguments (`'copy <source> [target]'`, `'<files...>'`);
112
- the words before the name are groups (`'package exe <file>'`). The other properties of the DTO are options
113
- (`--times 2`), the booleans flags (`--shout`); `.description()` of a schema is the help of its option, and the
114
- `description` action option the help of the command.
115
- - The value returned is written to stdout (a string as it is, other values as JSON); an error is written to stderr and
116
- the command ends with the code 1.
117
-
118
- ### Service: `@ServiceAction`
119
-
120
- ```text
121
- @ServiceAction(pattern, DTOClass?, options?)
122
- ```
123
-
124
- - The pattern is an object (nested 2 levels at most) whose keys a message must hold with the same values; a string
125
- value may be a glob (`'user.*'`). The most specific matching pattern wins. The input is the message without the keys
126
- of the pattern.
127
- - The caller receives `{success: true, error: null, payload}`, or `{success: false, error, payload: null}` where
128
- `error` holds the properties of the error (`errno`, `errMsg`, `statusCode`, `message`…); JSON-RPC answers with its
129
- own `result` and `error`. A message no pattern matches fails with `ControllerActionNotFoundException`.
130
-
131
- ### Schedules: `@Schedule`
132
-
133
- ```text
134
- @Schedule(schedule, options?)
135
- ```
136
-
137
- The method runs on a schedule when the entrypoint has a schedule adapter (`schedule: scheduleEntrypoint()`). The
138
- schedule is a duration (`'30s'`, `'1h30m'`, an interval), a cron expression (`'0 */5 * * * *'`, 5 or 6 fields, or a
139
- macro such as `'@daily'`), or the options `{every}` or `{cron, timezone}` with:
140
-
141
- - `overlap` (default `false`): start a run while the previous one is in progress; otherwise the run is skipped;
142
- - `immediate` (default `false`): also run once as soon as the application launched;
143
- - `exclusive` (default `false`): run in one instance of the application at a time, with the locks of a mutex component
144
- (`true` uses the component named `mutex`, `{mutex, key}` names it and the lock); see [Mutex](Mutex.md).
145
-
146
- Each run has its own scope and goes through the middlewares and the access control; the method receives an empty
147
- input, `this.context` is a `ScheduleContext` (its `schedule`, its planned time, its `run` number). An error thrown is
148
- given to the `onError` option of `scheduleEntrypoint`, or logged, and the next runs go on.
149
-
150
- ```typescript
151
- import {Controller, ScheduleContext} from 'lakutata/com/entrypoint'
152
- import {Schedule} from 'lakutata/decorator/ctrl'
153
-
154
- class JobController extends Controller {
155
- //Every 30 seconds, and once as soon as the application launched
156
- @Schedule({every: '30s', immediate: true})
157
- public async heartbeat(): Promise<void> {
158
- if (this.context instanceof ScheduleContext) console.log(`run ${this.context.run} of ${this.context.schedule}`)
159
- }
160
-
161
- //At 03:00 every day in Shanghai, in one instance at a time (a mutex component named "mutex")
162
- @Schedule({cron: '0 3 * * *', timezone: 'Asia/Shanghai', exclusive: true}, {name: 'Purge the expired sessions'})
163
- public async purge(): Promise<void> {
164
- }
165
- }
166
- ```
167
-
168
- ### Prefixes: `@Prefix`
169
-
170
- `@Prefix({route, command, pattern})` on a controller class prefixes its actions: `route` is prepended to the HTTP
171
- routes, `command` to the CLI commands (joined by `:`, `user` and `get` give `user:get`), `pattern` is merged into the
172
- service patterns.
173
-
174
- ```typescript
175
- import {DTO} from 'lakutata'
176
- import {Controller} from 'lakutata/com/entrypoint'
177
- import {CLIAction, GET, Prefix, ServiceAction} from 'lakutata/decorator/ctrl'
178
- import {Expect} from 'lakutata/decorator/dto'
179
-
180
- class UserIdDTO extends DTO {
181
- @Expect(DTO.String().required())
182
- public id: string
183
- }
184
-
185
- @Prefix({route: '/users', command: 'user', pattern: {resource: 'user'}})
186
- class UserController extends Controller {
187
- //GET /users/:id, the command "user:get <id>" and the message {resource: 'user', act: 'get', id}
188
- @GET('/:id', UserIdDTO)
189
- @CLIAction('get <id>', UserIdDTO)
190
- @ServiceAction({act: 'get'}, UserIdDTO)
191
- public async get(input: UserIdDTO): Promise<{ id: string }> {
192
- return {id: input.id}
193
- }
194
- }
195
- ```
196
-
197
- ## 3. Middlewares and access control
198
-
199
- A middleware runs around an action, before its input validation and its access control: a function
200
- `(invocation, next) => Promise<unknown>` or a `Middleware` class. They are given to `buildEntrypoints({middlewares})`
201
- (every action), to an adapter's `middlewares` option, to `@UseMiddlewares(...)` on a controller class or a method, and to
202
- the `middlewares` action option.
203
-
204
- ```typescript
205
- import {type ActionInvocation, Controller, type Next} from 'lakutata/com/entrypoint'
206
- import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
207
-
208
- async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
209
- const started: number = Date.now()
210
- try {
211
- return await next()
212
- } finally {
213
- console.log(`${invocation.context.type} took ${Date.now() - started} ms`)
214
- }
215
- }
216
-
217
- @UseMiddlewares(timing)
218
- class HealthController extends Controller {
219
- @GET('/health')
220
- public async health(): Promise<string> {
221
- return 'ok'
222
- }
223
- }
224
- ```
225
-
226
- An action declared with `{acl: true}` is checked by the rules given to `buildEntrypoints({rules})`: `AccessControlRule`
227
- subclasses, created for each request, whose `rule()` resolving `false` denies it with an `AccessDeniedException` (HTTP
228
- 403). The `rule` action option is the action's own rule.
229
-
230
- ```typescript
231
- import {AccessControlRule, ContextType, Controller, type HTTPContext} from 'lakutata/com/entrypoint'
232
- import {DELETE} from 'lakutata/decorator/ctrl'
233
-
234
- class AdminRule extends AccessControlRule<HTTPContext> {
235
- public get supportContextTypes(): ContextType[] {
236
- return [ContextType.HTTP]
237
- }
238
-
239
- public async rule(): Promise<boolean> {
240
- if (!this.targetAction.groups.includes('admin')) return true
241
- return this.context.request.headers['x-role'] === 'admin'
242
- }
243
- }
244
-
245
- class UserController extends Controller {
246
- @DELETE('/users/:id', {acl: true, groups: ['admin']})
247
- public async remove(input: { id: string }): Promise<void> {
248
- }
249
- }
250
-
251
- //buildEntrypoints({controllers: [UserController], rules: [AdminRule], httpActionGroups: {admin: 'Administration'}})
252
- ```
253
-
254
- The groups of an action must be declared in `httpActionGroups`, `cliActionGroups` or `serviceActionGroups` of
255
- `buildEntrypoints`. `shutdownTimeout` (default 30000 ms) is the time given to the running requests when the entrypoint
256
- stops.
257
-
258
- ## 4. The transports
259
-
260
- | Option of `buildEntrypoints` | Adapter | Subpath | Install |
261
- |---|---|---|---|
262
- | `http` | `expressHTTPEntrypoint({port})` | `lakutata/adapter/express` | `express` |
263
- | `http` | `fastifyHTTPEntrypoint({port})` | `lakutata/adapter/fastify` | `fastify` |
264
- | `cli` | `commanderCLIEntrypoint()` | `lakutata/adapter/commander` | `commander` (`@inquirer/prompts` for the prompts) |
265
- | `service` | `socketIOServiceEntrypoint({port})` | `lakutata/adapter/socket.io` | `socket.io` |
266
- | `service` | `jsonRPCServiceEntrypoint()` (HTTP, WebSocket, stdio) | `lakutata/adapter/jsonrpc` | `ws` for WebSocket |
267
- | `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) |
268
- | `service` | `httpServiceEntrypoint({port})` (`POST /`, events by SSE) | `lakutata/com/entrypoint` | |
269
- | `service` | `grpcServiceEntrypoint({port})` | `@lakutata/grpc/service` | `@lakutata/grpc` |
270
- | `service` | `natsServiceEntrypoint('nats')` | `@lakutata/nats` | `@lakutata/nats` |
271
- | `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | |
272
-
273
- Each option takes one adapter or an array of adapters. `buildHTTPEntrypoint`, `buildCLIEntrypoint` and
274
- `buildServiceEntrypoint` (from `lakutata/com/entrypoint`) type an entrypoint function in the style of lakutata 2.x, to
275
- plug another library; `EntrypointAdapter` (from `@lakutata/entrypoint/channel`) is the interface of the adapters.
276
-
277
- ### From an entrypoint function of 2.x to the HTTP adapters
278
-
279
- An HTTP entrypoint function of 2.x (`buildHTTPEntrypoint`) keeps working: it is called in the startup of the entrypoint
280
- (the objects it gets, such as an authenticator injecting the entrypoint, are available) and awaited, so the application
281
- launches once its server listens. To move it to `expressHTTPEntrypoint`:
282
-
283
- - the middlewares and the routes of the application (CORS, compression, a health check, the authenticator, a web UI) go
284
- into its `before` hook, which gets the objects with `module.getObject()`; the body parsers are its `body` option
285
- (`{limit: '100mb'}`); the `keepAliveTimeout`/`headersTimeout` of the server its `server` option;
286
- - the input built from the request (the query, the body, a value of the session) is its `input` option;
287
- - the `{errno, errMsg, data}` envelope is `jsonEnvelope()`. The applications of 2.x usually answered the errors of the
288
- actions with 200 and the errors of their express middlewares (`next(error)`: an authenticator's 401) with their
289
- status: `jsonEnvelope({actionErrorStatus: 200})` does the same — `errorStatus: 200` would answer the 401 with 200 too,
290
- which the clients redirecting to the login page on a 401 would miss;
291
- - the other fields of the envelope of the application (`timestamp`, `nonce`, a request id) are given by its `extra`
292
- option, called for each response, results and errors: `jsonEnvelope({actionErrorStatus: 200, extra: () =>
293
- ({timestamp: Date.now()})})`. They are added after `errno`, `errMsg` and `data`, which they never replace — no
294
- responder of its own is needed to keep them.
295
-
296
- ```typescript
297
- import {buildEntrypoints} from 'lakutata/com/entrypoint'
298
- import {expressHTTPEntrypoint, jsonEnvelope} from 'lakutata/adapter/express'
299
- import type {Express, NextFunction, Request, Response} from 'express'
300
-
301
- class Unauthorized extends Error {
302
- public readonly errno: string = 'E_UNAUTHORIZED'
303
-
304
- public readonly statusCode: number = 401
305
- }
306
-
307
- export const entrypoint = buildEntrypoints({
308
- controllers: [],
309
- http: expressHTTPEntrypoint({
310
- port: 3000,
311
- body: {limit: '100mb'},
312
- before: async (app: Express): Promise<void> => {
313
- app.get('/health', (_request: Request, response: Response): unknown => response.json({status: 'ok'}))
314
- //The authenticator of the requests: its errors keep their status (401)
315
- app.use((request: Request, _response: Response, next: NextFunction): void => request.headers.authorization ? next() : next(new Unauthorized('Not logged in')))
316
- },
317
- //The errors of the actions answered with 200 in the envelope, as in 2.x, with its timestamp
318
- respond: jsonEnvelope({actionErrorStatus: 200, extra: (): Record<string, unknown> => ({timestamp: Date.now()})})
319
- })
320
- })
321
- ```
322
-
323
- `fastifyHTTPEntrypoint` takes the same options (`fastifyJSONEnvelope({actionErrorStatus: 200, extra})`, the authenticator
324
- being a hook of fastify). A responder of its own receives the origin of each error as the fourth argument of its
325
- `error` (`'action'` or `'request'`).
326
-
327
- ### The clients of the service actions and their events
328
-
329
- The service adapters with clients give the id of the client of a request to its context, `ServiceContext.clientId`:
330
- `electronIPCServiceEntrypoint` (each page of a window), `socketIOServiceEntrypoint` (each socket, its id given when it
331
- connects by its `auth.id` or its `id` header), `httpServiceEntrypoint` (the `id` header of the `POST` requests, the `id`
332
- header or query of the event stream `GET /events`), `jsonRPCServiceEntrypoint` over WebSocket and stdio, and
333
- `grpcServiceEntrypoint` (the `id` metadata). The entrypoint pushes events to a client with `notify(clientId, event,
334
- ...args)` (it returns whether a client received it) or to every client with `broadcast(event, ...args)` (it returns the
335
- number of clients):
336
-
337
- ```typescript
338
- import {Controller, Entrypoint, ServiceContext} from 'lakutata/com/entrypoint'
339
- import {ServiceAction} from 'lakutata/decorator/ctrl'
340
- import {Inject} from 'lakutata/decorator/di'
341
-
342
- class TaskController extends Controller {
343
- @Inject('entrypoint')
344
- protected readonly entrypoint: Entrypoint
345
-
346
- @ServiceAction({cmd: 'start'})
347
- public async start(): Promise<void> {
348
- const clientId: string | undefined = (this.context as ServiceContext).clientId
349
- if (clientId) setTimeout((): void => void this.entrypoint.notify(clientId, 'done', {task: 1}), 1000)
350
- this.entrypoint.broadcast('started', {task: 1})
351
- }
352
- }
353
- ```
354
-
355
- The renderers of Electron listen to the events with `window.ipc.on(event, listener)` (the Electron templates), the
356
- sockets with `socket.on(event, listener)`, the HTTP clients with the Server-Sent Events of `GET /events`
357
- (`EventSource`, curl). A page of a window is a client until it is loaded again or its window is closed.
358
-
359
- ### JSON-RPC 2.0
360
-
361
- `jsonRPCServiceEntrypoint` serves the service actions over JSON-RPC 2.0: the method and the params (by name) of a
362
- request are the pattern and the input of an action, `{"method": "order.get", "params": {"id": 1}}` being dispatched as
363
- `{method: 'order.get', id: 1}` (`methodField` names the field, `toPattern(method, params)` maps them to other patterns,
364
- `positionalParams` accepts the params by position). The batches and the notifications follow the specification; the
365
- errors are `-32601` (no action matches), `-32602` (the DTO refuses the input) and `-32000` (the errors of the actions,
366
- the whole error as their `data`). The transports are HTTP (`POST`), WebSocket (the package `ws`) and stdio (framed by
367
- lines or by `Content-Length` headers); the last two push the events of the clients as notifications. Over WebSocket, the
368
- connections that do not answer the pings are closed (`heartbeat`, 30 s); over stdio, the application exits once its
369
- input ends (`end: 'application'`).
370
-
371
- ```text
372
- service: [
373
- jsonRPCServiceEntrypoint({port: 4000, path: '/rpc'}),
374
- jsonRPCServiceEntrypoint({transport: 'websocket', port: 4001}),
375
- jsonRPCServiceEntrypoint({transport: 'stdio', framing: 'content-length'})
376
- ]
377
- ```
378
-
379
- ### gRPC and NATS
380
-
381
- `grpcServiceEntrypoint` (`@lakutata/grpc/service`) serves the service actions over gRPC: the service
382
- `lakutata.service.v1.Service` (`SERVICE_PROTO`, for the clients of other languages) invokes an action with the JSON of
383
- its pattern and input, and streams the events of a client. `GRPCServiceProxy`, declared with `buildGRPCServiceProxy`,
384
- invokes the actions of a remote application and listens to its events. The deadline of a call aborts its action
385
- (`this.signal`). See the README of `@lakutata/grpc`.
386
-
387
- ```typescript
388
- import {Application} from 'lakutata'
389
- import {buildGRPCServiceProxy, type GRPCServiceProxy} from '@lakutata/grpc/service'
390
-
391
- Application
392
- .run(() => ({
393
- id: 'shop.app',
394
- name: 'Shop',
395
- components: {
396
- orders: buildGRPCServiceProxy({address: 'orders.internal:50051'})
397
- }
398
- }))
399
- .onLaunched(async (app: Application): Promise<void> => {
400
- const orders: GRPCServiceProxy = await app.getObject<GRPCServiceProxy>('orders')
401
- orders.on('orderChanged', (order: { id: number }): void => console.log(order))
402
- console.log(await orders.invoke({cmd: 'order.get', id: 1}))
403
- })
404
- ```
405
-
406
- `natsServiceEntrypoint` (`@lakutata/nats`) serves them over NATS, on the subject of the application id, the instances
407
- sharing a queue group; `ServiceProxy`, declared with `buildServiceProxy`, invokes them. See the README of
408
- `@lakutata/nats`.