@lakutata/entrypoint 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 (129) hide show
  1. package/README.md +39 -26
  2. package/README.zh-CN.md +182 -0
  3. package/dist/cjs/Entrypoint.d.ts +13 -7
  4. package/dist/cjs/Entrypoint.js +12 -6
  5. package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
  6. package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.js +3 -2
  7. package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
  8. package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
  9. package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
  10. package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.js +8 -2
  11. package/dist/cjs/adapter/express/Responders.d.ts +2 -1
  12. package/dist/cjs/adapter/express/Responders.js +4 -2
  13. package/dist/cjs/adapter/express/StaticUI.d.ts +2 -1
  14. package/dist/cjs/adapter/express/StaticUI.js +2 -1
  15. package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
  16. package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
  17. package/dist/cjs/adapter/fastify/Responders.d.ts +2 -1
  18. package/dist/cjs/adapter/fastify/Responders.js +4 -2
  19. package/dist/cjs/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
  20. package/dist/cjs/adapter/http/HTTPServiceEntrypoint.js +10 -22
  21. package/dist/cjs/adapter/http/HTTPServiceProxy.d.ts +190 -0
  22. package/dist/cjs/adapter/http/HTTPServiceProxy.js +371 -0
  23. package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
  24. package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +15 -26
  25. package/dist/cjs/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
  26. package/dist/cjs/adapter/schedule/ScheduleEntrypoint.js +12 -5
  27. package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
  28. package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
  29. package/dist/cjs/channel/BuiltinChannels.js +4 -2
  30. package/dist/cjs/channel/EntrypointAdapter.d.ts +2 -2
  31. package/dist/cjs/decorators/HTTPAction.d.ts +5 -5
  32. package/dist/cjs/decorators/Schedule.d.ts +4 -2
  33. package/dist/cjs/decorators/Schedule.js +3 -2
  34. package/dist/cjs/decorators/ServiceAction.d.ts +12 -8
  35. package/dist/cjs/decorators/http/DELETE.d.ts +5 -5
  36. package/dist/cjs/decorators/http/GET.d.ts +7 -7
  37. package/dist/cjs/decorators/http/HEAD.d.ts +9 -9
  38. package/dist/cjs/decorators/http/OPTIONS.d.ts +8 -7
  39. package/dist/cjs/decorators/http/PATCH.d.ts +5 -5
  40. package/dist/cjs/decorators/http/POST.d.ts +5 -5
  41. package/dist/cjs/decorators/http/PUT.d.ts +7 -7
  42. package/dist/cjs/exceptions/AccessDeniedException.d.ts +7 -1
  43. package/dist/cjs/exceptions/AccessDeniedException.js +8 -3
  44. package/dist/cjs/exceptions/EntrypointStoppedException.d.ts +2 -1
  45. package/dist/cjs/exceptions/EntrypointStoppedException.js +2 -1
  46. package/dist/cjs/exceptions/InvalidActionGroupException.d.ts +4 -0
  47. package/dist/cjs/exceptions/InvalidActionGroupException.js +4 -0
  48. package/dist/cjs/exceptions/ServiceUnavailableException.d.ts +15 -0
  49. package/dist/cjs/exceptions/ServiceUnavailableException.js +18 -0
  50. package/dist/cjs/exports/Entrypoint.d.ts +3 -0
  51. package/dist/cjs/exports/Entrypoint.js +6 -1
  52. package/dist/cjs/lib/AccessControl.d.ts +2 -1
  53. package/dist/cjs/lib/AccessControl.js +2 -1
  54. package/dist/cjs/lib/AccessControlRule.d.ts +2 -1
  55. package/dist/cjs/lib/AccessControlRule.js +2 -1
  56. package/dist/cjs/lib/ExplicitStatus.d.ts +11 -0
  57. package/dist/cjs/lib/ExplicitStatus.js +22 -0
  58. package/dist/cjs/lib/Middleware.d.ts +2 -1
  59. package/dist/cjs/lib/Middleware.js +2 -1
  60. package/dist/cjs/lib/Preflight.d.ts +8 -0
  61. package/dist/cjs/lib/Preflight.js +13 -0
  62. package/dist/cjs/lib/ReadBody.d.ts +9 -0
  63. package/dist/cjs/lib/ReadBody.js +25 -0
  64. package/dist/cjs/lib/pattern/FlattenPattern.d.ts +14 -0
  65. package/dist/cjs/lib/pattern/FlattenPattern.js +59 -0
  66. package/dist/esm/Entrypoint.js +12 -6
  67. package/dist/esm/adapter/commander/CommanderCLIEntrypoint.js +3 -2
  68. package/dist/esm/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
  69. package/dist/esm/adapter/express/ExpressHTTPEntrypoint.js +8 -2
  70. package/dist/esm/adapter/express/Responders.js +4 -2
  71. package/dist/esm/adapter/express/StaticUI.js +2 -1
  72. package/dist/esm/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
  73. package/dist/esm/adapter/fastify/Responders.js +4 -2
  74. package/dist/esm/adapter/http/HTTPServiceEntrypoint.js +9 -21
  75. package/dist/esm/adapter/http/HTTPServiceProxy.js +366 -0
  76. package/dist/esm/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +14 -25
  77. package/dist/esm/adapter/schedule/ScheduleEntrypoint.js +12 -5
  78. package/dist/esm/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
  79. package/dist/esm/channel/BuiltinChannels.js +4 -2
  80. package/dist/esm/decorators/Schedule.js +3 -2
  81. package/dist/esm/exceptions/AccessDeniedException.js +8 -3
  82. package/dist/esm/exceptions/EntrypointStoppedException.js +2 -1
  83. package/dist/esm/exceptions/InvalidActionGroupException.js +4 -0
  84. package/dist/esm/exceptions/ServiceUnavailableException.js +14 -0
  85. package/dist/esm/exports/Entrypoint.js +3 -0
  86. package/dist/esm/lib/AccessControl.js +2 -1
  87. package/dist/esm/lib/AccessControlRule.js +2 -1
  88. package/dist/esm/lib/ExplicitStatus.js +18 -0
  89. package/dist/esm/lib/Middleware.js +2 -1
  90. package/dist/esm/lib/Preflight.js +10 -0
  91. package/dist/esm/lib/ReadBody.js +22 -0
  92. package/dist/esm/lib/pattern/FlattenPattern.js +55 -0
  93. package/dist/types/Entrypoint.d.ts +13 -7
  94. package/dist/types/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
  95. package/dist/types/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
  96. package/dist/types/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
  97. package/dist/types/adapter/express/Responders.d.ts +2 -1
  98. package/dist/types/adapter/express/StaticUI.d.ts +2 -1
  99. package/dist/types/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
  100. package/dist/types/adapter/fastify/Responders.d.ts +2 -1
  101. package/dist/types/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
  102. package/dist/types/adapter/http/HTTPServiceProxy.d.ts +190 -0
  103. package/dist/types/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
  104. package/dist/types/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
  105. package/dist/types/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
  106. package/dist/types/channel/EntrypointAdapter.d.ts +2 -2
  107. package/dist/types/decorators/HTTPAction.d.ts +5 -5
  108. package/dist/types/decorators/Schedule.d.ts +4 -2
  109. package/dist/types/decorators/ServiceAction.d.ts +12 -8
  110. package/dist/types/decorators/http/DELETE.d.ts +5 -5
  111. package/dist/types/decorators/http/GET.d.ts +7 -7
  112. package/dist/types/decorators/http/HEAD.d.ts +9 -9
  113. package/dist/types/decorators/http/OPTIONS.d.ts +8 -7
  114. package/dist/types/decorators/http/PATCH.d.ts +5 -5
  115. package/dist/types/decorators/http/POST.d.ts +5 -5
  116. package/dist/types/decorators/http/PUT.d.ts +7 -7
  117. package/dist/types/exceptions/AccessDeniedException.d.ts +7 -1
  118. package/dist/types/exceptions/EntrypointStoppedException.d.ts +2 -1
  119. package/dist/types/exceptions/InvalidActionGroupException.d.ts +4 -0
  120. package/dist/types/exceptions/ServiceUnavailableException.d.ts +15 -0
  121. package/dist/types/exports/Entrypoint.d.ts +3 -0
  122. package/dist/types/lib/AccessControl.d.ts +2 -1
  123. package/dist/types/lib/AccessControlRule.d.ts +2 -1
  124. package/dist/types/lib/ExplicitStatus.d.ts +11 -0
  125. package/dist/types/lib/Middleware.d.ts +2 -1
  126. package/dist/types/lib/Preflight.d.ts +8 -0
  127. package/dist/types/lib/ReadBody.d.ts +9 -0
  128. package/dist/types/lib/pattern/FlattenPattern.d.ts +14 -0
  129. package/package.json +35 -4
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The headers of the answer to a preflight of a browser: the headers the request asks for are allowed (Authorization,
3
+ * the headers of the application…), the headers of the adapter when it asks for none
4
+ * @param request
5
+ * @param headers the CORS headers of the adapter
6
+ */
7
+ export function preflightHeaders(request, headers) {
8
+ const requested = request.headers['access-control-request-headers'];
9
+ return { ...headers, 'Access-Control-Allow-Headers': requested || headers['Access-Control-Allow-Headers'], 'Vary': 'Access-Control-Request-Headers' };
10
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Read the body of a request, up to a limit. Over the limit, it fails with a 413 error at once, and the rest of the body
3
+ * is read and dropped: the connection stays usable for the next request of the client (closing it while the client
4
+ * still sends would fail its request, keeping it unread would fail the next one)
5
+ * @param request
6
+ * @param limit bytes
7
+ */
8
+ export function readBody(request, limit) {
9
+ return new Promise((resolve, reject) => {
10
+ const chunks = [];
11
+ let size = 0;
12
+ const end = () => resolve(Buffer.concat(chunks).toString('utf8'));
13
+ const collect = (chunk) => {
14
+ size += chunk.length;
15
+ if (size <= limit)
16
+ return void chunks.push(chunk);
17
+ request.off('data', collect).off('end', end).on('data', () => undefined);
18
+ reject(Object.assign(new Error(`The body of the request is larger than ${limit} bytes`), { statusCode: 413 }));
19
+ };
20
+ request.on('data', collect).once('end', end).once('error', reject);
21
+ });
22
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The nesting levels of a subject flattened: deeper than the patterns may be (2 levels)
3
+ */
4
+ const MAX_DEPTH = 3;
5
+ /**
6
+ * Whether a value is a plain object, whose properties a pattern compares one by one
7
+ * @param value
8
+ */
9
+ function isPlainObject(value) {
10
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
11
+ return false;
12
+ const prototype = Object.getPrototypeOf(value);
13
+ return prototype === Object.prototype || prototype === null;
14
+ }
15
+ /**
16
+ * A pattern with the properties of its nested objects as keys joined by dots (`{res: {name: 'user'}}` gives
17
+ * `{'res.name': 'user'}`): their values are compared one by one, not as the text of their object. An empty nested
18
+ * object stays a value.
19
+ * @param pattern
20
+ * @param prefix
21
+ */
22
+ export function flattenPattern(pattern, prefix = '') {
23
+ const flat = {};
24
+ for (const [key, value] of Object.entries(pattern)) {
25
+ if (isPlainObject(value) && Object.keys(value).length) {
26
+ Object.assign(flat, flattenPattern(value, `${prefix}${key}.`));
27
+ }
28
+ else {
29
+ flat[`${prefix}${key}`] = value;
30
+ }
31
+ }
32
+ return flat;
33
+ }
34
+ /**
35
+ * A subject matched by the flattened patterns: its own properties, with the ones of its nested objects as keys joined
36
+ * by dots
37
+ * @param subject
38
+ */
39
+ export function flattenSubject(subject) {
40
+ if (!isPlainObject(subject))
41
+ return subject;
42
+ const flat = { ...subject };
43
+ const add = (object, prefix, depth) => {
44
+ for (const [key, value] of Object.entries(object)) {
45
+ if (!isPlainObject(value))
46
+ continue;
47
+ for (const [nestedKey, nestedValue] of Object.entries(value))
48
+ flat[`${prefix}${key}.${nestedKey}`] = nestedValue;
49
+ if (depth < MAX_DEPTH)
50
+ add(value, `${prefix}${key}.`, depth + 1);
51
+ }
52
+ };
53
+ add(subject, '', 1);
54
+ return flat;
55
+ }
@@ -60,7 +60,7 @@ export type ServiceEntrypoint = (module: Module, handler: ServiceEntrypointHandl
60
60
  export type CLIMap = Map<string, JSONSchema>;
61
61
  /**
62
62
  * The HTTP actions given to an {@link HTTPEntrypoint}: each route (with its controller prefix) mapped to the set of
63
- * its HTTP methods (`GET`, `POST`…).
63
+ * its HTTP methods, as declared (`get`, `post`… from `@GET`, `@POST`…; `@HTTPAction` keeps the case it is given).
64
64
  */
65
65
  export type HTTPRouteMap<HTTPMethods = string> = Map<string, Set<HTTPMethods>>;
66
66
  /**
@@ -303,7 +303,8 @@ export interface EntrypointActions<T extends BaseActionInfo> {
303
303
  * if (cliMap.has(command)) console.log(await handler(new CLIContext({command: command, data: {}})))
304
304
  * })
305
305
  * })
306
- * }
306
+ * },
307
+ * bootstrap: ['entrypoint']
307
308
  * }))
308
309
  * ```
309
310
  */
@@ -335,8 +336,9 @@ export declare const buildCLIEntrypoint: (entrypoint: CLIEntrypoint) => CLIEntry
335
336
  * controllers: [HealthController],
336
337
  * http: buildHTTPEntrypoint((module, routeMap, handler, registerDestroy): void => {
337
338
  * const server: Server = createServer(async (request, response): Promise<void> => {
339
+ * //The route map holds the routes and the methods as declared: '/health', 'get'
338
340
  * const route: string = (request.url ?? '/').split('?')[0]
339
- * const method: string = request.method ?? 'GET'
341
+ * const method: string = (request.method ?? 'GET').toLowerCase()
340
342
  * if (!routeMap.get(route)?.has(method)) {
341
343
  * response.writeHead(404).end()
342
344
  * return
@@ -353,7 +355,8 @@ export declare const buildCLIEntrypoint: (entrypoint: CLIEntrypoint) => CLIEntry
353
355
  * registerDestroy((): Promise<void> => new Promise((resolve): void => void server.close((): void => resolve())))
354
356
  * })
355
357
  * })
356
- * }
358
+ * },
359
+ * bootstrap: ['entrypoint']
357
360
  * }))
358
361
  * ```
359
362
  */
@@ -394,7 +397,8 @@ export declare const buildHTTPEntrypoint: (entrypoint: HTTPEntrypoint) => HTTPEn
394
397
  * registerDestroy((): void => void bus.off('request', listener))
395
398
  * })
396
399
  * })
397
- * }
400
+ * },
401
+ * bootstrap: ['entrypoint']
398
402
  * }))
399
403
  * ```
400
404
  */
@@ -444,7 +448,8 @@ export declare const buildServiceEntrypoint: (entrypoint: ServiceEntrypoint) =>
444
448
  * http: expressHTTPEntrypoint({port: 3000}),
445
449
  * schedule: scheduleEntrypoint()
446
450
  * })
447
- * }
451
+ * },
452
+ * bootstrap: ['entrypoint']
448
453
  * }))
449
454
  * ```
450
455
  */
@@ -531,7 +536,8 @@ type StartedAdapter = {
531
536
  * http: expressHTTPEntrypoint({port: 3000}),
532
537
  * service: socketIOServiceEntrypoint({port: 3001})
533
538
  * })
534
- * }
539
+ * },
540
+ * bootstrap: ['entrypoint']
535
541
  * }))
536
542
  * .onLaunched(async (app: Application): Promise<void> => {
537
543
  * const entrypoint: Entrypoint = await app.getObject<Entrypoint>('entrypoint')
@@ -173,7 +173,7 @@ export type CommanderCLIEntrypointOptions = {
173
173
  * @CLIAction('greet <name>', GreetDTO)
174
174
  * public async greet(input: GreetDTO): Promise<string> {
175
175
  * const text: string = `Hello, ${input.name}`
176
- * return (input.loud ? text.toUpperCase() : text).repeat(input.times ?? 1)
176
+ * return Array(input.times ?? 1).fill(input.loud ? text.toUpperCase() : text).join('\n')
177
177
  * }
178
178
  * }
179
179
  *
@@ -185,7 +185,8 @@ export type CommanderCLIEntrypointOptions = {
185
185
  * controllers: [GreetController],
186
186
  * cli: commanderCLIEntrypoint({bin: 'tool', description: 'A greeting tool', version: '1.0.0'})
187
187
  * })
188
- * }
188
+ * },
189
+ * bootstrap: ['entrypoint']
189
190
  * })
190
191
  * ```
191
192
  */
@@ -187,7 +187,8 @@ export declare function ipcEventOfContext<Event extends IPCInvokeEvent = IPCInvo
187
187
  * controllers: [EchoController],
188
188
  * service: electronIPCServiceEntrypoint()
189
189
  * })
190
- * }
190
+ * },
191
+ * bootstrap: ['entrypoint']
191
192
  * })
192
193
  *
193
194
  * //In the preload script of a window
@@ -20,7 +20,7 @@ export type ExpressHookContext = {
20
20
  */
21
21
  readonly server: Server;
22
22
  /**
23
- * Each action route mapped to its HTTP methods, in lower case as declared (`'/users/:id'` → `{'get', 'delete'}`).
23
+ * Each action route mapped to its HTTP methods, as declared (`'/users/:id'` → `{'get', 'delete'}`).
24
24
  */
25
25
  readonly routes: Map<string, Set<string>>;
26
26
  /**
@@ -166,7 +166,8 @@ export type ExpressHTTPEntrypointOptions = {
166
166
  * fallback: staticUI('./ui/dist')
167
167
  * })
168
168
  * })
169
- * }
169
+ * },
170
+ * bootstrap: ['entrypoint']
170
171
  * })
171
172
  * ```
172
173
  */
@@ -100,7 +100,8 @@ export type JSONEnvelopeOptions = {
100
100
  * })
101
101
  * })
102
102
  * })
103
- * }
103
+ * },
104
+ * bootstrap: ['entrypoint']
104
105
  * })
105
106
  * ```
106
107
  */
@@ -76,7 +76,8 @@ export declare function assetsMaxAge(value: string | number | undefined): number
76
76
  * fallback: staticUI('./ui/dist', {assetsMaxAge: 86400})
77
77
  * })
78
78
  * })
79
- * }
79
+ * },
80
+ * bootstrap: ['entrypoint']
80
81
  * })
81
82
  * ```
82
83
  */
@@ -150,7 +150,8 @@ export type FastifyHTTPEntrypointOptions = {
150
150
  * }
151
151
  * })
152
152
  * })
153
- * }
153
+ * },
154
+ * bootstrap: ['entrypoint']
154
155
  * })
155
156
  * ```
156
157
  */
@@ -99,7 +99,8 @@ export type FastifyJSONEnvelopeOptions = {
99
99
  * })
100
100
  * })
101
101
  * })
102
- * }
102
+ * },
103
+ * bootstrap: ['entrypoint']
103
104
  * })
104
105
  * ```
105
106
  */
@@ -66,8 +66,8 @@ export declare function httpRequestOfContext(context: ServiceContext): IncomingM
66
66
  * A request is a `POST` to the path whose JSON body holds the action's pattern and input (`{"cmd": "echo", "text":
67
67
  * "hi"}`). The response is always 200 with the body `{success, error, payload}` (see `ServiceResponseCodec`): `payload`
68
68
  * is the result, or `error` the whole error object when the action failed (no matching action, validation…). A body that
69
- * is not JSON or exceeds `limit` is answered with 400 and an error body; any other method or path with 404. The CORS
70
- * headers allow any origin.
69
+ * is not JSON is answered with 400 and an error body, a body over `limit` with 413; any other method or path with 404.
70
+ * The CORS headers allow any origin, and the headers a preflight asks for (`Authorization`…).
71
71
  *
72
72
  * The events pushed to the clients (`Entrypoint.notify`, `Entrypoint.broadcast`) are streamed as Server-Sent Events by
73
73
  * `GET <path>/events`: each event's name is the SSE event, its arguments the JSON array of its data. A client gives its
@@ -102,7 +102,8 @@ export declare function httpRequestOfContext(context: ServiceContext): IncomingM
102
102
  * controllers: [EchoController],
103
103
  * service: httpServiceEntrypoint({port: 3000, path: '/rpc'})
104
104
  * })
105
- * }
105
+ * },
106
+ * bootstrap: ['entrypoint']
106
107
  * })
107
108
  *
108
109
  * //A client: the response is {"success": true, "error": null, "payload": "hi"}
@@ -0,0 +1,190 @@
1
+ import { type ActionPattern, Component, type ComponentOptionsBuilder } from '@lakutata/core';
2
+ /**
3
+ * The options of an {@link HTTPServiceProxy}, given to {@link buildHTTPServiceProxy}.
4
+ */
5
+ export type BuildHTTPServiceProxyOptions = {
6
+ /**
7
+ * The address of the service: the URL of its `httpServiceEntrypoint`, with the adapter's `path`
8
+ * (`http://inventory:3000/`, `https://orders.internal/rpc`).
9
+ */
10
+ readonly url: string;
11
+ /**
12
+ * The headers sent with each call and with the event stream, such as `{Authorization: 'Bearer <token>'}`.
13
+ */
14
+ readonly headers?: Record<string, string>;
15
+ /**
16
+ * The client's id, sent as the `id` header of the calls and the `id` query parameter of the event stream: the
17
+ * service's `ServiceContext.clientId`, which `Entrypoint.notify()` pushes events to. Give a stable one to receive
18
+ * the events across restarts. At most 200 characters.
19
+ * @default a random UUID
20
+ */
21
+ readonly clientId?: string;
22
+ /**
23
+ * The delay, in milliseconds, before the event stream is opened again once it ended or failed (the service
24
+ * restarted, unreachable). A non-negative integer.
25
+ * @default 1000
26
+ */
27
+ readonly reconnectDelay?: number;
28
+ };
29
+ /**
30
+ * Build the component options of an {@link HTTPServiceProxy}, to register it in the `components` of an application.
31
+ * @param options The service's address, and the headers, client id and reconnection delay.
32
+ * @returns The component options.
33
+ * @example
34
+ * ```typescript
35
+ * import {Application} from 'lakutata'
36
+ * import {buildHTTPServiceProxy, type HTTPServiceProxy} from 'lakutata/com/entrypoint'
37
+ *
38
+ * Application
39
+ * .run(() => ({
40
+ * id: 'shop.app',
41
+ * name: 'Shop',
42
+ * components: {
43
+ * inventory: buildHTTPServiceProxy({url: 'http://inventory.internal:3000/'}),
44
+ * //With a token, and a stable client id to receive the events of the service
45
+ * orders: buildHTTPServiceProxy({
46
+ * url: 'https://orders.internal/rpc',
47
+ * headers: {Authorization: `Bearer ${process.env.ORDERS_TOKEN}`},
48
+ * clientId: 'shop-1'
49
+ * })
50
+ * }
51
+ * }))
52
+ * .onLaunched(async (app: Application): Promise<void> => {
53
+ * const inventory: HTTPServiceProxy = await app.getObject<HTTPServiceProxy>('inventory')
54
+ * console.log(await inventory.invoke({cmd: 'stock.get', product: 'lamp'}))
55
+ * })
56
+ * ```
57
+ */
58
+ export declare const buildHTTPServiceProxy: ComponentOptionsBuilder<BuildHTTPServiceProxyOptions>;
59
+ /**
60
+ * The client of a lakutata service served over HTTP by `httpServiceEntrypoint`: it invokes the service's actions by
61
+ * their patterns and listens to the events the service pushes to it. It is a component (a singleton), registered with
62
+ * {@link buildHTTPServiceProxy}; it needs no package (it uses the `fetch()` of Node.js), and its event stream is opened
63
+ * when the first listener is added.
64
+ *
65
+ * The failures of the remote actions are thrown as `ServiceInvokeException`, with the remote error's message and
66
+ * properties (`errno`, `statusCode`…) and the address as its `service`; a service that gives no response (unreachable,
67
+ * too slow, or an answer that is not a service response) as a `ServiceUnavailableException`.
68
+ * @example
69
+ * ```typescript
70
+ * import {Component} from 'lakutata'
71
+ * import {type HTTPServiceProxy, ServiceInvokeException, ServiceUnavailableException} from 'lakutata/com/entrypoint'
72
+ * import {Inject} from 'lakutata/decorator/di'
73
+ *
74
+ * class StockWatcher extends Component {
75
+ * //Registered as inventory: buildHTTPServiceProxy({url: 'http://inventory.internal:3000/'})
76
+ * @Inject('inventory')
77
+ * protected readonly inventory: HTTPServiceProxy
78
+ *
79
+ * protected async init(): Promise<void> {
80
+ * //The events the inventory pushes with Entrypoint.notify(clientId, 'lowStock', product, quantity)
81
+ * this.inventory.on('lowStock', (product: string, quantity: number): void => {
82
+ * console.log(`${product}: ${quantity} left`)
83
+ * })
84
+ * }
85
+ *
86
+ * public async quantity(product: string): Promise<number | undefined> {
87
+ * try {
88
+ * //5 seconds at most; the inventory aborts the action's signal when the call gives up
89
+ * return await this.inventory.invoke({cmd: 'stock.get', product: product}, 5000)
90
+ * } catch (error) {
91
+ * //The action failed: its error
92
+ * if (error instanceof ServiceInvokeException && error.errno === 'E_PRODUCT_NOT_FOUND') return undefined
93
+ * //The inventory gave no response
94
+ * if (error instanceof ServiceUnavailableException) return undefined
95
+ * throw error
96
+ * }
97
+ * }
98
+ * }
99
+ * ```
100
+ */
101
+ export declare class HTTPServiceProxy extends Component {
102
+ #private;
103
+ /**
104
+ * The service's address, as configured (see {@link BuildHTTPServiceProxyOptions.url}); required.
105
+ */
106
+ protected readonly url: string;
107
+ /**
108
+ * The headers sent with each call and with the event stream; none by default.
109
+ */
110
+ protected readonly headers?: Record<string, string>;
111
+ /**
112
+ * The configured client id; a random UUID is used without it (see {@link HTTPServiceProxy.id}).
113
+ */
114
+ protected readonly clientId?: string;
115
+ /**
116
+ * The delay, in milliseconds, before the event stream is opened again.
117
+ * @default 1000
118
+ */
119
+ protected readonly reconnectDelay: number;
120
+ /**
121
+ * Create the client id and the address of the event stream (`<url>/events?id=<id>`); nothing connects before the
122
+ * first call.
123
+ * @protected
124
+ */
125
+ protected init(): Promise<void>;
126
+ /**
127
+ * Close the event stream and remove the listeners (the calls in progress complete).
128
+ * @protected
129
+ */
130
+ protected destroy(): Promise<void>;
131
+ /**
132
+ * The client's id: the configured `clientId`, or a random UUID. It is sent as the `id` header of the calls, and is
133
+ * the `ServiceContext.clientId` of the service's actions.
134
+ */
135
+ get id(): string;
136
+ /**
137
+ * Invoke an action of the service, matched by its pattern.
138
+ * @param input The action's pattern and its input in one object (`{cmd: 'order.get', id: 1}`), sent as JSON: the
139
+ * values JSON cannot hold (functions, `undefined`, class instances) are lost or turned into plain objects.
140
+ * @param timeout The longest wait for the response, in milliseconds; 600000 (10 minutes) by default. The request is
141
+ * abandoned after it, and the service aborts the action's signal.
142
+ * @returns The action's result, decoded from JSON: `undefined` when the action returned nothing.
143
+ * @throws {ServiceInvokeException} When the action fails (no action matches the pattern, an invalid input, an error
144
+ * thrown, the service stopping), or the service refuses the request (another path, a body over its limit): the
145
+ * remote error's message and properties (`errno`…), the address as its `service`.
146
+ * @throws {ServiceUnavailableException} When the service gives no response: unreachable, no response within the
147
+ * timeout, or an answer that is not a service response. Its `cause` is the error of the request.
148
+ */
149
+ invoke<T extends Record<string, any>>(input: ActionPattern<T>, timeout?: number): Promise<any>;
150
+ /**
151
+ * Listen to an event the service pushes to this client (`Entrypoint.notify()`, `broadcast()`); the first listener
152
+ * opens the event stream. The listeners' errors are ignored. The `client` event, received when the stream opens,
153
+ * carries the client's id.
154
+ * @param eventName The event's name.
155
+ * @param listener Called with the event's arguments, decoded from JSON.
156
+ * @returns The proxy, to chain the calls.
157
+ */
158
+ on(eventName: string, listener: (...args: any[]) => void): this;
159
+ /**
160
+ * Listen to an event the service pushes to this client, once: the listener is removed before it is called.
161
+ * @param eventName The event's name.
162
+ * @param listener Called with the event's arguments, decoded from JSON.
163
+ * @returns The proxy, to chain the calls.
164
+ */
165
+ once(eventName: string, listener: (...args: any[]) => void): this;
166
+ /**
167
+ * Stop listening to an event; removing the last listener of all the events closes the event stream.
168
+ * @param eventName The event's name.
169
+ * @param listener The listener to remove; without it, all the event's listeners are removed.
170
+ * @returns The proxy, to chain the calls.
171
+ */
172
+ off(eventName: string, listener?: (...args: any[]) => void): this;
173
+ /**
174
+ * Add a listener, and open the event stream when it is not open.
175
+ * @param eventName The event's name.
176
+ * @param listener The listener.
177
+ * @param once Whether the listener is removed after its first call.
178
+ * @returns The proxy.
179
+ * @protected
180
+ */
181
+ protected add(eventName: string, listener: (...args: any[]) => void, once: boolean): this;
182
+ /**
183
+ * Read the event stream and call the listeners; the stream is opened again after `reconnectDelay` milliseconds
184
+ * when it ends or fails, while there are listeners. It stops once no listener is left (removed by
185
+ * {@link HTTPServiceProxy.off}, or the last `once()` listener called) or the proxy is destroyed.
186
+ * @returns A promise resolved once the reading stops.
187
+ * @protected
188
+ */
189
+ protected stream(): Promise<void>;
190
+ }
@@ -16,7 +16,9 @@ export type JSONRPCServiceEntrypointOptions = JSONRPCMappingOptions & {
16
16
  /**
17
17
  * The transport of the messages: `'http'` (a `POST` per message), `'websocket'` (a message per WebSocket text
18
18
  * frame, with the events pushed; it needs the optional package `ws`), or `'stdio'` (the process's standard input
19
- * and output, for a child process, with the events pushed).
19
+ * and output, for a child process, with the events pushed). With `'stdio'`, the standard output belongs to the
20
+ * protocol: the application's logs must be written to the standard error
21
+ * (`{class: Logger, destinations: [process.stderr]}`), or they corrupt the messages.
20
22
  * @default 'http'
21
23
  */
22
24
  transport?: 'http' | 'websocket' | 'stdio';
@@ -95,11 +97,12 @@ export type JSONRPCServiceEntrypointOptions = JSONRPCMappingOptions & {
95
97
  * validation, otherwise `SERVER_ERROR` with the whole error object as its `data`.
96
98
  *
97
99
  * Over HTTP, a message is the body of a `POST` to the path, answered with 200 and the JSON response (204 when it held
98
- * only notifications); any other method or path is answered with 404, and the CORS headers allow any origin. Over
99
- * WebSocket and stdio, the events pushed by `Entrypoint.notify` and `Entrypoint.broadcast` are sent as notifications,
100
- * `{"jsonrpc": "2.0", "method": event, "params": args}`. The client id carried by `ServiceContext.clientId` is the `id`
101
- * header or `id` query parameter of the HTTP request or of the WebSocket connection (at most 200 characters; a random
102
- * one for a WebSocket connection without it), and `'stdio'` on stdio.
100
+ * only notifications); any other method or path is answered with 404. The CORS headers allow any origin, and the
101
+ * headers a preflight asks for (`Authorization`…). Over WebSocket and stdio, the events pushed by `Entrypoint.notify`
102
+ * and `Entrypoint.broadcast` are sent as notifications, `{"jsonrpc": "2.0", "method": event, "params": args}`. The
103
+ * client id carried by `ServiceContext.clientId` is the `id` header or `id` query parameter of the HTTP request or of
104
+ * the WebSocket connection (at most 200 characters; a random one for a WebSocket connection without it), and `'stdio'`
105
+ * on stdio.
103
106
  *
104
107
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
105
108
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
@@ -135,7 +138,8 @@ export type JSONRPCServiceEntrypointOptions = JSONRPCMappingOptions & {
135
138
  * jsonRPCServiceEntrypoint({name: 'rpc-ws', transport: 'websocket', port: 3001, path: '/rpc'})
136
139
  * ]
137
140
  * })
138
- * }
141
+ * },
142
+ * bootstrap: ['entrypoint']
139
143
  * })
140
144
  *
141
145
  * //An HTTP client: {"jsonrpc": "2.0", "id": 1, "result": {"id": 1, "name": "Ada"}}
@@ -79,7 +79,8 @@ export type ScheduleEntrypointOptions = {
79
79
  * onSkip: (action, scheduledAt, reason): void => console.warn(`${action.name} skipped (${reason})`)
80
80
  * })
81
81
  * })
82
- * }
82
+ * },
83
+ * bootstrap: ['entrypoint']
83
84
  * })
84
85
  * ```
85
86
  */
@@ -123,7 +123,8 @@ export declare function socketOfContext(context: ServiceContext): Socket | undef
123
123
  * controllers: [EchoController],
124
124
  * service: socketIOServiceEntrypoint({port: 3000, options: {cors: {origin: '*'}}})
125
125
  * })
126
- * }
126
+ * },
127
+ * bootstrap: ['entrypoint']
127
128
  * })
128
129
  *
129
130
  * //A client, identified as client-1 (the events pushed by entrypoint.notify('client-1', 'progress', 50) reach it)
@@ -72,8 +72,8 @@ export interface ChannelRuntime<Context extends BaseContext = BaseContext> {
72
72
  /**
73
73
  * Resolves once every adapter of the entrypoint started and the application launched (its objects and its
74
74
  * bootstrap are ready), for the adapters acting on their own rather than on requests (running a command,
75
- * triggering the schedules). Without an application (a bare container) it never resolves unless the entrypoint's
76
- * `launched()` is called.
75
+ * triggering the schedules). Without an application (a bare container), it resolves once the entrypoint is
76
+ * initialized.
77
77
  */
78
78
  readonly ready: Promise<void>;
79
79
  /**
@@ -6,11 +6,11 @@ import { HTTPContext } from '../context/HTTPContext.js';
6
6
  * Expose the controller method as an HTTP action answering a route for an HTTP method, served by the HTTP adapters of
7
7
  * the entrypoint (`expressHTTPEntrypoint`, `fastifyHTTPEntrypoint`). Without a DTO, the input is accepted as it is.
8
8
  *
9
- * On a request, the adapter assembles the input: by default, the query string merged with the parsed body (JSON or URL
10
- * encoded), then the route parameters over them. The middlewares run, the input is validated against the DTO (the
11
- * defaults applied, the unknown keys removed), the access control runs when `acl` is set, then the method receives the
12
- * validated input as a plain object, not a DTO instance. The query values and the route parameters are strings and the
13
- * DTO schemas are strict: convert them with `.strict(false)` (`DTO.Number().strict(false)`).
9
+ * On a request, the adapter assembles the input: by default, the query string merged with the parsed body (JSON; URL
10
+ * encoded with Express), then the route parameters over them. The middlewares run, the input is validated against the
11
+ * DTO (the defaults applied, the unknown keys removed), the access control runs when `acl` is set, then the method
12
+ * receives the validated input as a plain object, not a DTO instance. The query values and the route parameters are
13
+ * strings and the DTO schemas are strict: convert them with `.strict(false)` (`DTO.Number().strict(false)`).
14
14
  *
15
15
  * The value returned is answered by the adapter's responder: as JSON by default, with the status 204 for `undefined`;
16
16
  * return `Result.status()`, `Result.file()` or `Result.events()` for a status and headers, a file or server-sent
@@ -61,7 +61,8 @@ export type ScheduleOptions = ({
61
61
  * @param schedule A duration text (`'30s'`, `'1h30m'`) for an interval, any other text for a cron expression
62
62
  * (`'0 *\/5 * * * *'`, `'@daily'`), or the schedule options.
63
63
  * @returns The method decorator.
64
- * @throws {SyntaxError} When the class is declared, if the interval or the cron expression is invalid.
64
+ * @throws {SyntaxError} When the class is declared, if the interval or the cron expression is invalid, or if the
65
+ * expression matches no date (`'0 0 30 2 *'`).
65
66
  * @throws {RangeError} When the class is declared, if the time zone is unknown.
66
67
  * @example
67
68
  * ```typescript
@@ -113,7 +114,8 @@ export declare function Schedule<ClassPrototype extends Controller, Method>(sche
113
114
  * rules (false by default); `rule` is an access rule of the action; `middlewares` run before the ones `@UseMiddlewares`
114
115
  * adds to the method.
115
116
  * @returns The method decorator.
116
- * @throws {SyntaxError} When the class is declared, if the interval or the cron expression is invalid.
117
+ * @throws {SyntaxError} When the class is declared, if the interval or the cron expression is invalid, or if the
118
+ * expression matches no date (`'0 0 30 2 *'`).
117
119
  * @throws {RangeError} When the class is declared, if the time zone is unknown.
118
120
  * @throws {InvalidValueException} When the class is declared, if the action options are invalid.
119
121
  */
@@ -15,8 +15,9 @@ import { ServiceContext } from '../context/ServiceContext.js';
15
15
  * payload: null}`, `error` holding its properties (`errno`, `errMsg`, `statusCode`, `message`…); JSON-RPC answers it as
16
16
  * an error response. A message no pattern matches fails with `ControllerActionNotFoundException`.
17
17
  * @param pattern The pattern of the action: an object (nested 2 levels at most) whose keys the messages must hold with
18
- * the same values; a string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern
19
- * matching a message wins. The controller's `@Prefix({pattern})` is merged into it.
18
+ * the same values, the keys of a nested object too (`{res: {name: 'user'}}` does not match `{res: {name: 'order'}}`); a
19
+ * string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern matching a message
20
+ * wins. The controller's `@Prefix({pattern})` is merged into it.
20
21
  * @returns The method decorator.
21
22
  * @throws {InvalidActionPatternDepthException} When the class is declared, if the pattern is nested more than 2 levels.
22
23
  * @example
@@ -71,8 +72,9 @@ export declare function ServiceAction<ClassPrototype extends Controller, Method>
71
72
  * returned is sent back as `{success: true, error: null, payload}`, an error as `{success: false, error, payload:
72
73
  * null}` (a JSON-RPC result or error response).
73
74
  * @param pattern The pattern of the action: an object (nested 2 levels at most) whose keys the messages must hold with
74
- * the same values; a string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern
75
- * matching a message wins. The controller's `@Prefix({pattern})` is merged into it.
75
+ * the same values, the keys of a nested object too (`{res: {name: 'user'}}` does not match `{res: {name: 'order'}}`); a
76
+ * string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern matching a message
77
+ * wins. The controller's `@Prefix({pattern})` is merged into it.
76
78
  * @param dtoConstructor The DTO class validating the input: the message without the keys of the pattern.
77
79
  * @returns The method decorator.
78
80
  * @throws {InvalidActionPatternDepthException} When the class is declared, if the pattern is nested more than 2 levels.
@@ -83,8 +85,9 @@ export declare function ServiceAction<ClassPrototype extends Controller, Method,
83
85
  * without the keys of the pattern, is accepted as it is. The value returned is sent back as `{success: true, error:
84
86
  * null, payload}`, an error as `{success: false, error, payload: null}` (a JSON-RPC result or error response).
85
87
  * @param pattern The pattern of the action: an object (nested 2 levels at most) whose keys the messages must hold with
86
- * the same values; a string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern
87
- * matching a message wins. The controller's `@Prefix({pattern})` is merged into it.
88
+ * the same values, the keys of a nested object too (`{res: {name: 'user'}}` does not match `{res: {name: 'order'}}`); a
89
+ * string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern matching a message
90
+ * wins. The controller's `@Prefix({pattern})` is merged into it.
88
91
  * @param actionOptions The action options: `acl` runs the access control rules (false by default); `name` (unique in
89
92
  * the service actions, `ClassName:method` by default), `description` and `groups` describe the action; `rule` is an
90
93
  * access rule of the action; `middlewares` run before the ones `@UseMiddlewares` adds to the method.
@@ -101,8 +104,9 @@ export declare function ServiceAction<ClassPrototype extends Controller, Method>
101
104
  * plain object. The value returned is sent back as `{success: true, error: null, payload}`, an error as `{success:
102
105
  * false, error, payload: null}` (a JSON-RPC result or error response).
103
106
  * @param pattern The pattern of the action: an object (nested 2 levels at most) whose keys the messages must hold with
104
- * the same values; a string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern
105
- * matching a message wins. The controller's `@Prefix({pattern})` is merged into it.
107
+ * the same values, the keys of a nested object too (`{res: {name: 'user'}}` does not match `{res: {name: 'order'}}`); a
108
+ * string value may be a glob (`'user.*'`, `*` any characters, `?` one). The most specific pattern matching a message
109
+ * wins. The controller's `@Prefix({pattern})` is merged into it.
106
110
  * @param dtoConstructor The DTO class validating the input: the message without the keys of the pattern.
107
111
  * @param actionOptions The action options: `acl` runs the access control rules (false by default); `name` (unique in
108
112
  * the service actions, `ClassName:method` by default), `description` and `groups` describe the action; `rule` is an