@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.
- 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,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
|
|
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"}}
|
|
@@ -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
|
|
170
|
-
* WebSocket and stdio, the events pushed by `Entrypoint.notify`
|
|
171
|
-
* `{"jsonrpc": "2.0", "method": event, "params": args}`. The
|
|
172
|
-
* header or `id` query parameter of the HTTP request or of
|
|
173
|
-
* one for a WebSocket connection without it), and `'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.
|
|
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);
|
|
@@ -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
|
-
*
|
|
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
|
|
107
|
-
|
|
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
|
|
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
|
*/
|
|
@@ -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)
|