@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.
- package/README.md +39 -26
- package/README.zh-CN.md +182 -0
- package/dist/cjs/Entrypoint.d.ts +13 -7
- package/dist/cjs/Entrypoint.js +12 -6
- package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
- package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.js +3 -2
- package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
- package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
- package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
- package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.js +8 -2
- package/dist/cjs/adapter/express/Responders.d.ts +2 -1
- package/dist/cjs/adapter/express/Responders.js +4 -2
- package/dist/cjs/adapter/express/StaticUI.d.ts +2 -1
- package/dist/cjs/adapter/express/StaticUI.js +2 -1
- package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
- package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
- package/dist/cjs/adapter/fastify/Responders.d.ts +2 -1
- package/dist/cjs/adapter/fastify/Responders.js +4 -2
- package/dist/cjs/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
- package/dist/cjs/adapter/http/HTTPServiceEntrypoint.js +10 -22
- package/dist/cjs/adapter/http/HTTPServiceProxy.d.ts +190 -0
- package/dist/cjs/adapter/http/HTTPServiceProxy.js +371 -0
- package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
- package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +15 -26
- package/dist/cjs/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
- package/dist/cjs/adapter/schedule/ScheduleEntrypoint.js +12 -5
- package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
- package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
- package/dist/cjs/channel/BuiltinChannels.js +4 -2
- package/dist/cjs/channel/EntrypointAdapter.d.ts +2 -2
- package/dist/cjs/decorators/HTTPAction.d.ts +5 -5
- package/dist/cjs/decorators/Schedule.d.ts +4 -2
- package/dist/cjs/decorators/Schedule.js +3 -2
- package/dist/cjs/decorators/ServiceAction.d.ts +12 -8
- package/dist/cjs/decorators/http/DELETE.d.ts +5 -5
- package/dist/cjs/decorators/http/GET.d.ts +7 -7
- package/dist/cjs/decorators/http/HEAD.d.ts +9 -9
- package/dist/cjs/decorators/http/OPTIONS.d.ts +8 -7
- package/dist/cjs/decorators/http/PATCH.d.ts +5 -5
- package/dist/cjs/decorators/http/POST.d.ts +5 -5
- package/dist/cjs/decorators/http/PUT.d.ts +7 -7
- package/dist/cjs/exceptions/AccessDeniedException.d.ts +7 -1
- package/dist/cjs/exceptions/AccessDeniedException.js +8 -3
- package/dist/cjs/exceptions/EntrypointStoppedException.d.ts +2 -1
- package/dist/cjs/exceptions/EntrypointStoppedException.js +2 -1
- package/dist/cjs/exceptions/InvalidActionGroupException.d.ts +4 -0
- package/dist/cjs/exceptions/InvalidActionGroupException.js +4 -0
- package/dist/cjs/exceptions/ServiceUnavailableException.d.ts +15 -0
- package/dist/cjs/exceptions/ServiceUnavailableException.js +18 -0
- package/dist/cjs/exports/Entrypoint.d.ts +3 -0
- package/dist/cjs/exports/Entrypoint.js +6 -1
- package/dist/cjs/lib/AccessControl.d.ts +2 -1
- package/dist/cjs/lib/AccessControl.js +2 -1
- package/dist/cjs/lib/AccessControlRule.d.ts +2 -1
- package/dist/cjs/lib/AccessControlRule.js +2 -1
- package/dist/cjs/lib/ExplicitStatus.d.ts +11 -0
- package/dist/cjs/lib/ExplicitStatus.js +22 -0
- package/dist/cjs/lib/Middleware.d.ts +2 -1
- package/dist/cjs/lib/Middleware.js +2 -1
- package/dist/cjs/lib/Preflight.d.ts +8 -0
- package/dist/cjs/lib/Preflight.js +13 -0
- package/dist/cjs/lib/ReadBody.d.ts +9 -0
- package/dist/cjs/lib/ReadBody.js +25 -0
- package/dist/cjs/lib/pattern/FlattenPattern.d.ts +14 -0
- package/dist/cjs/lib/pattern/FlattenPattern.js +59 -0
- package/dist/esm/Entrypoint.js +12 -6
- package/dist/esm/adapter/commander/CommanderCLIEntrypoint.js +3 -2
- package/dist/esm/adapter/electron/ElectronIPCServiceEntrypoint.js +2 -1
- package/dist/esm/adapter/express/ExpressHTTPEntrypoint.js +8 -2
- package/dist/esm/adapter/express/Responders.js +4 -2
- package/dist/esm/adapter/express/StaticUI.js +2 -1
- package/dist/esm/adapter/fastify/FastifyHTTPEntrypoint.js +4 -1
- package/dist/esm/adapter/fastify/Responders.js +4 -2
- package/dist/esm/adapter/http/HTTPServiceEntrypoint.js +9 -21
- package/dist/esm/adapter/http/HTTPServiceProxy.js +366 -0
- package/dist/esm/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +14 -25
- package/dist/esm/adapter/schedule/ScheduleEntrypoint.js +12 -5
- package/dist/esm/adapter/socket.io/SocketIOServiceEntrypoint.js +2 -1
- package/dist/esm/channel/BuiltinChannels.js +4 -2
- package/dist/esm/decorators/Schedule.js +3 -2
- package/dist/esm/exceptions/AccessDeniedException.js +8 -3
- package/dist/esm/exceptions/EntrypointStoppedException.js +2 -1
- package/dist/esm/exceptions/InvalidActionGroupException.js +4 -0
- package/dist/esm/exceptions/ServiceUnavailableException.js +14 -0
- package/dist/esm/exports/Entrypoint.js +3 -0
- package/dist/esm/lib/AccessControl.js +2 -1
- package/dist/esm/lib/AccessControlRule.js +2 -1
- package/dist/esm/lib/ExplicitStatus.js +18 -0
- package/dist/esm/lib/Middleware.js +2 -1
- package/dist/esm/lib/Preflight.js +10 -0
- package/dist/esm/lib/ReadBody.js +22 -0
- package/dist/esm/lib/pattern/FlattenPattern.js +55 -0
- package/dist/types/Entrypoint.d.ts +13 -7
- package/dist/types/adapter/commander/CommanderCLIEntrypoint.d.ts +3 -2
- package/dist/types/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +2 -1
- package/dist/types/adapter/express/ExpressHTTPEntrypoint.d.ts +3 -2
- package/dist/types/adapter/express/Responders.d.ts +2 -1
- package/dist/types/adapter/express/StaticUI.d.ts +2 -1
- package/dist/types/adapter/fastify/FastifyHTTPEntrypoint.d.ts +2 -1
- package/dist/types/adapter/fastify/Responders.d.ts +2 -1
- package/dist/types/adapter/http/HTTPServiceEntrypoint.d.ts +4 -3
- package/dist/types/adapter/http/HTTPServiceProxy.d.ts +190 -0
- package/dist/types/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +11 -7
- package/dist/types/adapter/schedule/ScheduleEntrypoint.d.ts +2 -1
- package/dist/types/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +2 -1
- package/dist/types/channel/EntrypointAdapter.d.ts +2 -2
- package/dist/types/decorators/HTTPAction.d.ts +5 -5
- package/dist/types/decorators/Schedule.d.ts +4 -2
- package/dist/types/decorators/ServiceAction.d.ts +12 -8
- package/dist/types/decorators/http/DELETE.d.ts +5 -5
- package/dist/types/decorators/http/GET.d.ts +7 -7
- package/dist/types/decorators/http/HEAD.d.ts +9 -9
- package/dist/types/decorators/http/OPTIONS.d.ts +8 -7
- package/dist/types/decorators/http/PATCH.d.ts +5 -5
- package/dist/types/decorators/http/POST.d.ts +5 -5
- package/dist/types/decorators/http/PUT.d.ts +7 -7
- package/dist/types/exceptions/AccessDeniedException.d.ts +7 -1
- package/dist/types/exceptions/EntrypointStoppedException.d.ts +2 -1
- package/dist/types/exceptions/InvalidActionGroupException.d.ts +4 -0
- package/dist/types/exceptions/ServiceUnavailableException.d.ts +15 -0
- package/dist/types/exports/Entrypoint.d.ts +3 -0
- package/dist/types/lib/AccessControl.d.ts +2 -1
- package/dist/types/lib/AccessControlRule.d.ts +2 -1
- package/dist/types/lib/ExplicitStatus.d.ts +11 -0
- package/dist/types/lib/Middleware.d.ts +2 -1
- package/dist/types/lib/Preflight.d.ts +8 -0
- package/dist/types/lib/ReadBody.d.ts +9 -0
- package/dist/types/lib/pattern/FlattenPattern.d.ts +14 -0
- 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`, `
|
|
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).
|
|
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,
|
|
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
|
*/
|
|
@@ -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
|
|
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
|
|
99
|
-
* WebSocket and stdio, the events pushed by `Entrypoint.notify`
|
|
100
|
-
* `{"jsonrpc": "2.0", "method": event, "params": args}`. The
|
|
101
|
-
* header or `id` query parameter of the HTTP request or of
|
|
102
|
-
* one for a WebSocket connection without it), and `'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"}}
|
|
@@ -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
|
|
76
|
-
*
|
|
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
|
|
10
|
-
* encoded), then the route parameters over them. The middlewares run, the input is validated against the
|
|
11
|
-
* defaults applied, the unknown keys removed), the access control runs when `acl` is set, then the method
|
|
12
|
-
* validated input as a plain object, not a DTO instance. The query values and the route parameters are
|
|
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
|
|
19
|
-
*
|
|
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
|
|
75
|
-
*
|
|
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
|
|
87
|
-
*
|
|
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
|
|
105
|
-
*
|
|
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
|