@lakutata/entrypoint 3.0.0-beta.6 → 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,371 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HTTPServiceProxy = exports.buildHTTPServiceProxy = void 0;
13
+ const node_crypto_1 = require("node:crypto");
14
+ const core_1 = require("@lakutata/core");
15
+ const di_1 = require("@lakutata/core/decorator/di");
16
+ const ServiceResponseCodec_js_1 = require("../../lib/ServiceResponseCodec.js");
17
+ const ServiceUnavailableException_js_1 = require("../../exceptions/ServiceUnavailableException.js");
18
+ /**
19
+ * Build the component options of an {@link HTTPServiceProxy}, to register it in the `components` of an application.
20
+ * @param options The service's address, and the headers, client id and reconnection delay.
21
+ * @returns The component options.
22
+ * @example
23
+ * ```typescript
24
+ * import {Application} from 'lakutata'
25
+ * import {buildHTTPServiceProxy, type HTTPServiceProxy} from 'lakutata/com/entrypoint'
26
+ *
27
+ * Application
28
+ * .run(() => ({
29
+ * id: 'shop.app',
30
+ * name: 'Shop',
31
+ * components: {
32
+ * inventory: buildHTTPServiceProxy({url: 'http://inventory.internal:3000/'}),
33
+ * //With a token, and a stable client id to receive the events of the service
34
+ * orders: buildHTTPServiceProxy({
35
+ * url: 'https://orders.internal/rpc',
36
+ * headers: {Authorization: `Bearer ${process.env.ORDERS_TOKEN}`},
37
+ * clientId: 'shop-1'
38
+ * })
39
+ * }
40
+ * }))
41
+ * .onLaunched(async (app: Application): Promise<void> => {
42
+ * const inventory: HTTPServiceProxy = await app.getObject<HTTPServiceProxy>('inventory')
43
+ * console.log(await inventory.invoke({cmd: 'stock.get', product: 'lamp'}))
44
+ * })
45
+ * ```
46
+ */
47
+ const buildHTTPServiceProxy = (options) => {
48
+ return {
49
+ class: HTTPServiceProxy,
50
+ url: options.url,
51
+ headers: options.headers,
52
+ clientId: options.clientId,
53
+ reconnectDelay: options.reconnectDelay
54
+ };
55
+ };
56
+ exports.buildHTTPServiceProxy = buildHTTPServiceProxy;
57
+ /**
58
+ * Whether a body is a service response
59
+ * @param body
60
+ */
61
+ function isServiceResponse(body) {
62
+ return !!body && typeof body === 'object' && typeof body.success === 'boolean';
63
+ }
64
+ /**
65
+ * The reason a request failed: the network error under the "fetch failed" of fetch(), each of them when several
66
+ * addresses were tried (localhost: ::1, then 127.0.0.1)
67
+ * @param error
68
+ */
69
+ function reasonOf(error) {
70
+ const cause = error?.cause ?? error;
71
+ if (cause instanceof AggregateError && cause.errors.length)
72
+ return cause.errors.map((each) => each?.message ?? String(each)).join(', ');
73
+ return cause?.message || error?.message || String(error);
74
+ }
75
+ /**
76
+ * The events of a stream of Server-Sent Events: their names and their data, the comments skipped
77
+ * @param stream
78
+ */
79
+ async function* serverSentEvents(stream) {
80
+ const reader = stream.getReader();
81
+ const decoder = new TextDecoder();
82
+ let buffer = '';
83
+ let event = 'message';
84
+ let data = [];
85
+ try {
86
+ for (;;) {
87
+ const { value, done } = await reader.read();
88
+ if (done)
89
+ return;
90
+ buffer += decoder.decode(value, { stream: true });
91
+ //The complete lines; the last one may be cut by the chunk
92
+ const lines = buffer.split('\n');
93
+ buffer = lines.pop();
94
+ for (const read of lines) {
95
+ const line = read.endsWith('\r') ? read.slice(0, -1) : read;
96
+ //A blank line ends the event
97
+ if (!line) {
98
+ if (data.length)
99
+ yield { event: event, data: data.join('\n') };
100
+ event = 'message';
101
+ data = [];
102
+ continue;
103
+ }
104
+ if (line.startsWith(':'))
105
+ continue;
106
+ const colon = line.indexOf(':');
107
+ const field = colon < 0 ? line : line.slice(0, colon);
108
+ const value = colon < 0 ? '' : line.slice(colon + 1).replace(/^ /, '');
109
+ if (field === 'event')
110
+ event = value;
111
+ else if (field === 'data')
112
+ data.push(value);
113
+ }
114
+ }
115
+ }
116
+ finally {
117
+ reader.releaseLock();
118
+ }
119
+ }
120
+ /**
121
+ * The client of a lakutata service served over HTTP by `httpServiceEntrypoint`: it invokes the service's actions by
122
+ * their patterns and listens to the events the service pushes to it. It is a component (a singleton), registered with
123
+ * {@link buildHTTPServiceProxy}; it needs no package (it uses the `fetch()` of Node.js), and its event stream is opened
124
+ * when the first listener is added.
125
+ *
126
+ * The failures of the remote actions are thrown as `ServiceInvokeException`, with the remote error's message and
127
+ * properties (`errno`, `statusCode`…) and the address as its `service`; a service that gives no response (unreachable,
128
+ * too slow, or an answer that is not a service response) as a `ServiceUnavailableException`.
129
+ * @example
130
+ * ```typescript
131
+ * import {Component} from 'lakutata'
132
+ * import {type HTTPServiceProxy, ServiceInvokeException, ServiceUnavailableException} from 'lakutata/com/entrypoint'
133
+ * import {Inject} from 'lakutata/decorator/di'
134
+ *
135
+ * class StockWatcher extends Component {
136
+ * //Registered as inventory: buildHTTPServiceProxy({url: 'http://inventory.internal:3000/'})
137
+ * @Inject('inventory')
138
+ * protected readonly inventory: HTTPServiceProxy
139
+ *
140
+ * protected async init(): Promise<void> {
141
+ * //The events the inventory pushes with Entrypoint.notify(clientId, 'lowStock', product, quantity)
142
+ * this.inventory.on('lowStock', (product: string, quantity: number): void => {
143
+ * console.log(`${product}: ${quantity} left`)
144
+ * })
145
+ * }
146
+ *
147
+ * public async quantity(product: string): Promise<number | undefined> {
148
+ * try {
149
+ * //5 seconds at most; the inventory aborts the action's signal when the call gives up
150
+ * return await this.inventory.invoke({cmd: 'stock.get', product: product}, 5000)
151
+ * } catch (error) {
152
+ * //The action failed: its error
153
+ * if (error instanceof ServiceInvokeException && error.errno === 'E_PRODUCT_NOT_FOUND') return undefined
154
+ * //The inventory gave no response
155
+ * if (error instanceof ServiceUnavailableException) return undefined
156
+ * throw error
157
+ * }
158
+ * }
159
+ * }
160
+ * ```
161
+ */
162
+ class HTTPServiceProxy extends core_1.Component {
163
+ #id;
164
+ #eventsURL;
165
+ #listeners = new Map();
166
+ #events;
167
+ #destroyed = false;
168
+ /**
169
+ * Create the client id and the address of the event stream (`<url>/events?id=<id>`); nothing connects before the
170
+ * first call.
171
+ * @protected
172
+ */
173
+ async init() {
174
+ this.#id = this.clientId ?? (0, node_crypto_1.randomUUID)();
175
+ const events = new URL(this.url);
176
+ events.pathname = `${events.pathname.replace(/\/+$/, '')}/events`;
177
+ events.searchParams.set('id', this.#id);
178
+ this.#eventsURL = events.href;
179
+ }
180
+ /**
181
+ * Close the event stream and remove the listeners (the calls in progress complete).
182
+ * @protected
183
+ */
184
+ async destroy() {
185
+ this.#destroyed = true;
186
+ this.#events?.abort();
187
+ this.#listeners.clear();
188
+ }
189
+ /**
190
+ * The client's id: the configured `clientId`, or a random UUID. It is sent as the `id` header of the calls, and is
191
+ * the `ServiceContext.clientId` of the service's actions.
192
+ */
193
+ get id() {
194
+ return this.#id;
195
+ }
196
+ /**
197
+ * Invoke an action of the service, matched by its pattern.
198
+ * @param input The action's pattern and its input in one object (`{cmd: 'order.get', id: 1}`), sent as JSON: the
199
+ * values JSON cannot hold (functions, `undefined`, class instances) are lost or turned into plain objects.
200
+ * @param timeout The longest wait for the response, in milliseconds; 600000 (10 minutes) by default. The request is
201
+ * abandoned after it, and the service aborts the action's signal.
202
+ * @returns The action's result, decoded from JSON: `undefined` when the action returned nothing.
203
+ * @throws {ServiceInvokeException} When the action fails (no action matches the pattern, an invalid input, an error
204
+ * thrown, the service stopping), or the service refuses the request (another path, a body over its limit): the
205
+ * remote error's message and properties (`errno`…), the address as its `service`.
206
+ * @throws {ServiceUnavailableException} When the service gives no response: unreachable, no response within the
207
+ * timeout, or an answer that is not a service response. Its `cause` is the error of the request.
208
+ */
209
+ async invoke(input, timeout = 10 * 60 * 1000) {
210
+ let status;
211
+ let text;
212
+ try {
213
+ const response = await fetch(this.url, {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json', ...this.headers, 'id': this.#id },
216
+ body: JSON.stringify(input),
217
+ signal: AbortSignal.timeout(timeout)
218
+ });
219
+ status = response.status;
220
+ text = await response.text();
221
+ }
222
+ catch (error) {
223
+ const exception = error?.name === 'TimeoutError'
224
+ ? new ServiceUnavailableException_js_1.ServiceUnavailableException('The service {service} gave no response within {timeout} ms', { service: this.url, timeout: timeout })
225
+ : new ServiceUnavailableException_js_1.ServiceUnavailableException('The service {service} cannot be reached: {reason}', { service: this.url, reason: reasonOf(error) });
226
+ exception.service = this.url;
227
+ exception.cause = error;
228
+ throw exception;
229
+ }
230
+ let body;
231
+ try {
232
+ body = JSON.parse(text);
233
+ }
234
+ catch {
235
+ //Not JSON: below
236
+ }
237
+ if (!isServiceResponse(body)) {
238
+ const exception = new ServiceUnavailableException_js_1.ServiceUnavailableException('The service {service} answered {status} without a service response', { service: this.url, status: status });
239
+ exception.service = this.url;
240
+ throw exception;
241
+ }
242
+ return ServiceResponseCodec_js_1.ServiceResponseCodec.decode(body, this.url);
243
+ }
244
+ /**
245
+ * Listen to an event the service pushes to this client (`Entrypoint.notify()`, `broadcast()`); the first listener
246
+ * opens the event stream. The listeners' errors are ignored. The `client` event, received when the stream opens,
247
+ * carries the client's id.
248
+ * @param eventName The event's name.
249
+ * @param listener Called with the event's arguments, decoded from JSON.
250
+ * @returns The proxy, to chain the calls.
251
+ */
252
+ on(eventName, listener) {
253
+ return this.add(eventName, listener, false);
254
+ }
255
+ /**
256
+ * Listen to an event the service pushes to this client, once: the listener is removed before it is called.
257
+ * @param eventName The event's name.
258
+ * @param listener Called with the event's arguments, decoded from JSON.
259
+ * @returns The proxy, to chain the calls.
260
+ */
261
+ once(eventName, listener) {
262
+ return this.add(eventName, listener, true);
263
+ }
264
+ /**
265
+ * Stop listening to an event; removing the last listener of all the events closes the event stream.
266
+ * @param eventName The event's name.
267
+ * @param listener The listener to remove; without it, all the event's listeners are removed.
268
+ * @returns The proxy, to chain the calls.
269
+ */
270
+ off(eventName, listener) {
271
+ const listeners = this.#listeners.get(eventName);
272
+ if (listeners && listener)
273
+ listeners.forEach((entry) => entry.listener === listener && listeners.delete(entry));
274
+ if (!listener || !listeners?.size)
275
+ this.#listeners.delete(eventName);
276
+ if (!this.#listeners.size) {
277
+ this.#events?.abort();
278
+ this.#events = undefined;
279
+ }
280
+ return this;
281
+ }
282
+ /**
283
+ * Add a listener, and open the event stream when it is not open.
284
+ * @param eventName The event's name.
285
+ * @param listener The listener.
286
+ * @param once Whether the listener is removed after its first call.
287
+ * @returns The proxy.
288
+ * @protected
289
+ */
290
+ add(eventName, listener, once) {
291
+ if (!this.#listeners.has(eventName))
292
+ this.#listeners.set(eventName, new Set());
293
+ this.#listeners.get(eventName).add({ listener: listener, once: once });
294
+ if (!this.#events)
295
+ void this.stream();
296
+ return this;
297
+ }
298
+ /**
299
+ * Read the event stream and call the listeners; the stream is opened again after `reconnectDelay` milliseconds
300
+ * when it ends or fails, while there are listeners. It stops once no listener is left (removed by
301
+ * {@link HTTPServiceProxy.off}, or the last `once()` listener called) or the proxy is destroyed.
302
+ * @returns A promise resolved once the reading stops.
303
+ * @protected
304
+ */
305
+ async stream() {
306
+ const abort = new AbortController();
307
+ this.#events = abort;
308
+ while (!abort.signal.aborted && !this.#destroyed && this.#listeners.size) {
309
+ try {
310
+ const response = await fetch(this.#eventsURL, { headers: { ...this.headers, 'Accept': 'text/event-stream' }, signal: abort.signal });
311
+ if (response.ok && response.body) {
312
+ for await (const { event, data } of serverSentEvents(response.body)) {
313
+ const listeners = this.#listeners.get(event);
314
+ if (!listeners)
315
+ continue;
316
+ let args;
317
+ try {
318
+ args = JSON.parse(data);
319
+ }
320
+ catch {
321
+ continue;
322
+ }
323
+ for (const entry of [...listeners]) {
324
+ if (entry.once)
325
+ listeners.delete(entry);
326
+ try {
327
+ entry.listener(...Array.isArray(args) ? args : [args]);
328
+ }
329
+ catch {
330
+ //The errors of the listeners are theirs
331
+ }
332
+ }
333
+ if (!listeners.size)
334
+ this.#listeners.delete(event);
335
+ //The last listener gone (a once listener called): the stream is closed
336
+ if (!this.#listeners.size)
337
+ abort.abort();
338
+ }
339
+ }
340
+ else {
341
+ await response.body?.cancel();
342
+ }
343
+ }
344
+ catch {
345
+ //The service is gone: listened again after the delay
346
+ }
347
+ if (abort.signal.aborted || this.#destroyed)
348
+ break;
349
+ await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay));
350
+ }
351
+ if (this.#events === abort)
352
+ this.#events = undefined;
353
+ }
354
+ }
355
+ exports.HTTPServiceProxy = HTTPServiceProxy;
356
+ __decorate([
357
+ (0, di_1.Configurable)(core_1.DTO.String().uri({ scheme: ['http', 'https'] }).required()),
358
+ __metadata("design:type", String)
359
+ ], HTTPServiceProxy.prototype, "url", void 0);
360
+ __decorate([
361
+ (0, di_1.Configurable)(core_1.DTO.Object().pattern(core_1.DTO.String(), core_1.DTO.String()).optional()),
362
+ __metadata("design:type", Object)
363
+ ], HTTPServiceProxy.prototype, "headers", void 0);
364
+ __decorate([
365
+ (0, di_1.Configurable)(core_1.DTO.String().max(200).optional()),
366
+ __metadata("design:type", String)
367
+ ], HTTPServiceProxy.prototype, "clientId", void 0);
368
+ __decorate([
369
+ (0, di_1.Configurable)(core_1.DTO.Number().integer().min(0).optional().default(1000)),
370
+ __metadata("design:type", Number)
371
+ ], HTTPServiceProxy.prototype, "reconnectDelay", void 0);
@@ -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"}}
@@ -40,8 +40,11 @@ const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
40
40
  const JSONRPCProtocol_js_1 = require("./JSONRPCProtocol.js");
41
41
  const ApplicationOf_js_1 = require("../../lib/ApplicationOf.js");
42
42
  const Connections_js_1 = require("../../lib/Connections.js");
43
+ const ReadBody_js_1 = require("../../lib/ReadBody.js");
44
+ const Preflight_js_1 = require("../../lib/Preflight.js");
43
45
  /**
44
- * The headers of the responses of HTTP: the tools of the browsers may call the adapter
46
+ * The headers of the responses of HTTP: the tools of the browsers may call the adapter; a preflight is allowed the
47
+ * headers it asks for
45
48
  */
46
49
  const CORS_HEADERS = {
47
50
  'Access-Control-Allow-Origin': '*',
@@ -58,22 +61,6 @@ function clientIdOf(request) {
58
61
  const id = Array.isArray(given) ? given[0] : given ?? undefined;
59
62
  return id && id.length <= 200 ? id : undefined;
60
63
  }
61
- /**
62
- * The body of a request, up to a limit
63
- * @param request
64
- * @param limit
65
- */
66
- async function readBody(request, limit) {
67
- const chunks = [];
68
- let size = 0;
69
- for await (const chunk of request) {
70
- size += chunk.length;
71
- if (size > limit)
72
- throw Object.assign(new Error(`The body of the request is larger than ${limit} bytes`), { status: 413 });
73
- chunks.push(chunk);
74
- }
75
- return Buffer.concat(chunks).toString('utf8');
76
- }
77
64
  /**
78
65
  * The messages of a stream framed by lines, or by their Content-Length headers. A message larger than the limit is
79
66
  * skipped (the rest of its line, or the length its header declares, keeping the framing) and reported
@@ -166,11 +153,12 @@ function readMessages(input, framing, limit, receive, ended, oversized) {
166
153
  * validation, otherwise `SERVER_ERROR` with the whole error object as its `data`.
167
154
  *
168
155
  * Over HTTP, a message is the body of a `POST` to the path, answered with 200 and the JSON response (204 when it held
169
- * only notifications); any other method or path is answered with 404, and the CORS headers allow any origin. Over
170
- * WebSocket and stdio, the events pushed by `Entrypoint.notify` and `Entrypoint.broadcast` are sent as notifications,
171
- * `{"jsonrpc": "2.0", "method": event, "params": args}`. The client id carried by `ServiceContext.clientId` is the `id`
172
- * header or `id` query parameter of the HTTP request or of the WebSocket connection (at most 200 characters; a random
173
- * one for a WebSocket connection without it), and `'stdio'` on stdio.
156
+ * only notifications); any other method or path is answered with 404. The CORS headers allow any origin, and the
157
+ * headers a preflight asks for (`Authorization`…). Over WebSocket and stdio, the events pushed by `Entrypoint.notify`
158
+ * and `Entrypoint.broadcast` are sent as notifications, `{"jsonrpc": "2.0", "method": event, "params": args}`. The
159
+ * client id carried by `ServiceContext.clientId` is the `id` header or `id` query parameter of the HTTP request or of
160
+ * the WebSocket connection (at most 200 characters; a random one for a WebSocket connection without it), and `'stdio'`
161
+ * on stdio.
174
162
  *
175
163
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
176
164
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
@@ -206,7 +194,8 @@ function readMessages(input, framing, limit, receive, ended, oversized) {
206
194
  * jsonRPCServiceEntrypoint({name: 'rpc-ws', transport: 'websocket', port: 3001, path: '/rpc'})
207
195
  * ]
208
196
  * })
209
- * }
197
+ * },
198
+ * bootstrap: ['entrypoint']
210
199
  * })
211
200
  *
212
201
  * //An HTTP client: {"jsonrpc": "2.0", "id": 1, "result": {"id": 1, "name": "Ada"}}
@@ -304,7 +293,7 @@ function jsonRPCServiceEntrypoint(options = {}) {
304
293
  server = (0, node_http_1.createServer)((request, response) => {
305
294
  const url = new URL(request.url ?? '/', 'http://localhost');
306
295
  if (request.method === 'OPTIONS')
307
- return void response.writeHead(204, CORS_HEADERS).end();
296
+ return void response.writeHead(204, (0, Preflight_js_1.preflightHeaders)(request, CORS_HEADERS)).end();
308
297
  if (request.method !== 'POST' || url.pathname !== path) {
309
298
  return void response.writeHead(404, { ...CORS_HEADERS, 'Content-Type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: `POST ${path} receives the requests of JSON-RPC 2.0` } }));
310
299
  }
@@ -313,7 +302,7 @@ function jsonRPCServiceEntrypoint(options = {}) {
313
302
  if (!response.writableFinished)
314
303
  aborted.abort();
315
304
  });
316
- readBody(request, limit).then((body) => {
305
+ (0, ReadBody_js_1.readBody)(request, limit).then((body) => {
317
306
  void answer(runtime, body, { clientId: clientIdOf(request), extra: { request: request }, signal: aborted.signal }, (message) => {
318
307
  response.writeHead(200, { ...CORS_HEADERS, 'Content-Type': 'application/json; charset=utf-8' }).end(message);
319
308
  }).then(() => {
@@ -322,7 +311,7 @@ function jsonRPCServiceEntrypoint(options = {}) {
322
311
  response.writeHead(204, CORS_HEADERS).end();
323
312
  });
324
313
  }, (error) => {
325
- response.writeHead(error.status ?? 400, { ...CORS_HEADERS, 'Content-Type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: error.message } }));
314
+ response.writeHead(error.statusCode ?? 400, { ...CORS_HEADERS, 'Content-Type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: error.message } }));
326
315
  });
327
316
  });
328
317
  return await listen(server);
@@ -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
  */
@@ -64,7 +64,8 @@ const MAX_DELAY = 2 ** 31 - 1;
64
64
  * onSkip: (action, scheduledAt, reason): void => console.warn(`${action.name} skipped (${reason})`)
65
65
  * })
66
66
  * })
67
- * }
67
+ * },
68
+ * bootstrap: ['entrypoint']
68
69
  * })
69
70
  * ```
70
71
  */
@@ -95,16 +96,22 @@ function scheduleEntrypoint(options = {}) {
95
96
  timers.add(timer);
96
97
  };
97
98
  /**
98
- * Write a failed run to the log of the application
99
+ * Give a failed run to the onError handler, or write it to the log of the application; an error of the handler is
100
+ * written to the log
99
101
  * @param runtime
100
102
  * @param error
101
103
  * @param action
102
104
  * @param context
103
105
  */
104
106
  const report = async (runtime, error, action, context) => {
105
- if (options.onError)
106
- return await options.onError(error, action, context);
107
- await log(runtime, error, `The scheduled action ${action.name} (${context.schedule}) failed`);
107
+ if (!options.onError)
108
+ return await log(runtime, error, `The scheduled action ${action.name} (${context.schedule}) failed`);
109
+ try {
110
+ await options.onError(error, action, context);
111
+ }
112
+ catch (handlerError) {
113
+ await log(runtime, handlerError, `The onError handler of the scheduled action ${action.name} failed`);
114
+ }
108
115
  };
109
116
  /**
110
117
  * Write an error to the log of the application
@@ -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)
@@ -83,7 +83,8 @@ function clientIdOf(socket) {
83
83
  * controllers: [EchoController],
84
84
  * service: socketIOServiceEntrypoint({port: 3000, options: {cors: {origin: '*'}}})
85
85
  * })
86
- * }
86
+ * },
87
+ * bootstrap: ['entrypoint']
87
88
  * })
88
89
  *
89
90
  * //A client, identified as client-1 (the events pushed by entrypoint.notify('client-1', 'progress', 50) reach it)
@@ -4,6 +4,7 @@ exports.ScheduleChannel = exports.ServiceChannel = exports.CLIChannel = exports.
4
4
  const utils_1 = require("@lakutata/utils");
5
5
  const Channel_js_1 = require("./Channel.js");
6
6
  const PatternManager_js_1 = require("../lib/pattern/PatternManager.js");
7
+ const FlattenPattern_js_1 = require("../lib/pattern/FlattenPattern.js");
7
8
  const StringifyPattern_js_1 = require("../lib/StringifyPattern.js");
8
9
  const Prefix_js_1 = require("../lib/Prefix.js");
9
10
  const BaseContext_js_1 = require("../context/BaseContext.js");
@@ -77,12 +78,13 @@ exports.ServiceChannel = (0, Channel_js_1.defineChannel)({
77
78
  describe: (pattern) => ({ pattern: pattern }),
78
79
  prefix: (prefix, pattern) => prefix.pattern === undefined ? undefined : (0, Prefix_js_1.prefixPattern)(prefix.pattern, pattern),
79
80
  createMatcher: (entries) => {
81
+ //The values of the nested objects are compared one by one: {res: {name: 'user'}} matches as {'res.name': 'user'}
80
82
  const patterns = new PatternManager_js_1.PatternManager();
81
83
  for (const entry of entries)
82
- patterns.add(entry.details.pattern, entry);
84
+ patterns.add((0, FlattenPattern_js_1.flattenPattern)(entry.details.pattern), entry);
83
85
  return {
84
86
  match: (context) => {
85
- const entry = patterns.find(context.input);
87
+ const entry = patterns.find((0, FlattenPattern_js_1.flattenSubject)(context.input));
86
88
  return entry ? { entry: entry } : undefined;
87
89
  }
88
90
  };
@@ -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
  */
@@ -35,8 +35,9 @@ function ScheduleAddress(schedule) {
35
35
  address.schedule = `every ${typeof options.every === 'number' ? `${options.every}ms` : options.every}`;
36
36
  }
37
37
  else if (options.cron !== undefined) {
38
- //The expression is checked now, not at the first run
39
- new Cron_js_1.Cron(options.cron, options.timezone);
38
+ //The expression is checked now, not at the first run; a valid one matching no date (February 30th) would never run
39
+ if (!new Cron_js_1.Cron(options.cron, options.timezone).next())
40
+ throw new SyntaxError(`The cron expression ${options.cron} matches no date`);
40
41
  address.cron = options.cron;
41
42
  address.schedule = `cron ${options.cron}`;
42
43
  if (options.timezone)