@jsenv/server 15.5.3 → 16.0.1
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 +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +19 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- package/src/sse/sse_room.js +0 -313
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
|
3
|
+
*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createLogger } from "@jsenv/humanize";
|
|
7
|
+
import { createObservable } from "./interfacing_with_node/observable.js";
|
|
8
|
+
import { WebSocketResponse } from "./web_socket_response.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a Server-Sent Events controller that manages client connections and event distribution.
|
|
12
|
+
* Supports both SSE (EventSource) and WebSocket connections with automatic event history tracking.
|
|
13
|
+
*
|
|
14
|
+
* @class
|
|
15
|
+
* @param {Object} options - Configuration options for the SSE controller
|
|
16
|
+
* @param {String} [options.logLevel] - Controls logging verbosity ('debug', 'info', 'warn', 'error', etc.)
|
|
17
|
+
* @param {Boolean} [options.keepProcessAlive=false] - If true, prevents Node.js from exiting while SSE connections are active
|
|
18
|
+
* @param {Number} [options.keepaliveDuration=30000] - Milliseconds between keepalive messages to prevent connection timeout
|
|
19
|
+
* @param {Number} [options.retryDuration=1000] - Suggested client reconnection delay in milliseconds
|
|
20
|
+
* @param {Number} [options.historyLength=1000] - Maximum number of events to keep in history for reconnecting clients
|
|
21
|
+
* @param {Number} [options.maxClientAllowed=100] - Maximum number of concurrent client connections allowed
|
|
22
|
+
* @param {Function} [options.computeEventId] - Function to generate event IDs, receives (event, lastEventId) and returns new ID
|
|
23
|
+
* @param {Boolean} [options.welcomeEventEnabled=false] - Whether to send a welcome event to new clients
|
|
24
|
+
* @param {Boolean} [options.welcomeEventPublic=false] - If true, welcome events are broadcast to all clients, not just the new one
|
|
25
|
+
* @param {String} [options.actionOnClientLimitReached='refuse'] - Action when client limit is reached ('refuse' or 'kick-oldest')
|
|
26
|
+
*
|
|
27
|
+
* @returns {Object} SSE controller with the following methods:
|
|
28
|
+
* @returns {Function} .sendEventToAllClients - Sends an event to all connected clients
|
|
29
|
+
* @returns {Function} .fetch - Handles HTTP requests and upgrades them to SSE/WebSocket connections
|
|
30
|
+
* @returns {Function} .getAllEventSince - Retrieves events since a specific ID
|
|
31
|
+
* @returns {Function} .getClientCount - Returns the number of connected clients
|
|
32
|
+
* @returns {Function} .close - Closes all connections and stops the controller
|
|
33
|
+
* @returns {Function} .open - Reopens the controller after closing
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* import { ServerEvents } from "@jsenv/server";
|
|
37
|
+
*
|
|
38
|
+
* const serverEvents = new ServerEvents({
|
|
39
|
+
* welcomeEventEnabled: true,
|
|
40
|
+
* historyLength: 50
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Send events to all connected clients
|
|
44
|
+
* serverEvents.sendEventToAllClients({
|
|
45
|
+
* type: "update",
|
|
46
|
+
* data: JSON.stringify({ timestamp: Date.now() })
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Use in server route
|
|
50
|
+
* {
|
|
51
|
+
* endpoint: "GET /events",
|
|
52
|
+
* response: (request) => serverEvents.fetch(request)
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
export class ServerEvents {
|
|
56
|
+
constructor(...args) {
|
|
57
|
+
// eslint-disable-next-line no-constructor-return
|
|
58
|
+
return createServerEvents(...args);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a minimal Server-Sent Events controller that exposes only the fetch method
|
|
64
|
+
* to handle client connections, keeping other controller methods private.
|
|
65
|
+
*
|
|
66
|
+
* This is useful when you want to provide a minimal API surface and ensure
|
|
67
|
+
* the events can only be pushed through a given function.
|
|
68
|
+
*
|
|
69
|
+
* @class
|
|
70
|
+
* @param {Function} producer - Function called when first client connects
|
|
71
|
+
* @param {Object} [options] - Configuration options for the SSE controller
|
|
72
|
+
* @param {String} [options.logLevel] - Controls logging verbosity ('debug', 'info', 'warn', 'error', etc.)
|
|
73
|
+
* @param {Boolean} [options.keepProcessAlive=false] - If true, prevents Node.js from exiting while SSE connections are active
|
|
74
|
+
* @param {Number} [options.keepaliveDuration=30000] - Milliseconds between keepalive messages to prevent connection timeout
|
|
75
|
+
* @param {Number} [options.retryDuration=1000] - Suggested client reconnection delay in milliseconds
|
|
76
|
+
* @param {Number} [options.historyLength=1000] - Maximum number of events to keep in history for reconnecting clients
|
|
77
|
+
* @param {Number} [options.maxClientAllowed=100] - Maximum number of concurrent client connections allowed
|
|
78
|
+
* @param {Function} [options.computeEventId] - Function to generate event IDs, receives (event, lastEventId) and returns new ID
|
|
79
|
+
* @param {Boolean} [options.welcomeEventEnabled=false] - Whether to send a welcome event to new clients
|
|
80
|
+
* @param {Boolean} [options.welcomeEventPublic=false] - If true, welcome events are broadcast to all clients, not just the new one
|
|
81
|
+
* @param {String} [options.actionOnClientLimitReached='refuse'] - Action when client limit is reached ('refuse' or 'kick-oldest')
|
|
82
|
+
*
|
|
83
|
+
* @returns {Object} An object with only the fetch method to handle client connections
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* import { LazyServerEvents } from "@jsenv/server";
|
|
87
|
+
*
|
|
88
|
+
* // Events can only be sent through the producer function
|
|
89
|
+
* const events = new LazyServerEvents((api) => {
|
|
90
|
+
* console.log("First client connected");
|
|
91
|
+
*
|
|
92
|
+
* // Setup interval, database watchers, etc.
|
|
93
|
+
* const interval = setInterval(() => {
|
|
94
|
+
* api.sendEvent({
|
|
95
|
+
* type: "tick",
|
|
96
|
+
* data: new Date().toISOString()
|
|
97
|
+
* });
|
|
98
|
+
* }, 1000);
|
|
99
|
+
*
|
|
100
|
+
* // Return cleanup function that runs when last client disconnects
|
|
101
|
+
* return () => {
|
|
102
|
+
* console.log("Last client disconnected");
|
|
103
|
+
* clearInterval(interval);
|
|
104
|
+
* };
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* // Use in server route
|
|
108
|
+
* {
|
|
109
|
+
* endpoint: "GET /events",
|
|
110
|
+
* response: (request) => events.fetch(request)
|
|
111
|
+
* }
|
|
112
|
+
*/
|
|
113
|
+
export class LazyServerEvents {
|
|
114
|
+
constructor(producer, options = {}) {
|
|
115
|
+
const serverEvents = createServerEvents({
|
|
116
|
+
producer,
|
|
117
|
+
...options,
|
|
118
|
+
});
|
|
119
|
+
// eslint-disable-next-line no-constructor-return
|
|
120
|
+
return {
|
|
121
|
+
fetch: serverEvents.fetch,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const createServerEvents = ({
|
|
127
|
+
producer,
|
|
128
|
+
logLevel,
|
|
129
|
+
// do not keep process alive because of event source, something else must keep it alive
|
|
130
|
+
keepProcessAlive = false,
|
|
131
|
+
keepaliveDuration = 30 * 1000,
|
|
132
|
+
retryDuration = 1 * 1000,
|
|
133
|
+
historyLength = 1 * 1000,
|
|
134
|
+
maxClientAllowed = 100, // max 100 clients accepted
|
|
135
|
+
computeEventId = (event, lastEventId) => lastEventId + 1,
|
|
136
|
+
welcomeEventEnabled = false,
|
|
137
|
+
welcomeEventPublic = false,
|
|
138
|
+
actionOnClientLimitReached = "refuse",
|
|
139
|
+
} = {}) => {
|
|
140
|
+
const logger = createLogger({ logLevel });
|
|
141
|
+
|
|
142
|
+
const serverEventSource = {
|
|
143
|
+
closed: false,
|
|
144
|
+
};
|
|
145
|
+
const clientArray = [];
|
|
146
|
+
const eventHistory = createEventHistory(historyLength);
|
|
147
|
+
// what about previousEventId that keeps growing ?
|
|
148
|
+
// we could add some limit
|
|
149
|
+
// one limit could be that an event older than 24h is deleted
|
|
150
|
+
let previousEventId = 0;
|
|
151
|
+
let interval;
|
|
152
|
+
let producerReturnValue;
|
|
153
|
+
|
|
154
|
+
const addClient = (client) => {
|
|
155
|
+
if (clientArray.length === 0) {
|
|
156
|
+
if (typeof producer === "function") {
|
|
157
|
+
producerReturnValue = producer({
|
|
158
|
+
sendEvent: sendEventToAllClients,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
clientArray.push(client);
|
|
163
|
+
logger.debug(
|
|
164
|
+
`A client has joined. Number of client: ${clientArray.length}`,
|
|
165
|
+
);
|
|
166
|
+
if (client.lastKnownId !== undefined) {
|
|
167
|
+
const previousEvents = getAllEventSince(client.lastKnownId);
|
|
168
|
+
const eventMissedCount = previousEvents.length;
|
|
169
|
+
if (eventMissedCount > 0) {
|
|
170
|
+
logger.info(
|
|
171
|
+
`send ${eventMissedCount} event missed by client since event with id "${client.lastKnownId}"`,
|
|
172
|
+
);
|
|
173
|
+
for (const previousEvent of previousEvents) {
|
|
174
|
+
client.sendEvent(previousEvent);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (welcomeEventEnabled) {
|
|
179
|
+
const welcomeEvent = {
|
|
180
|
+
retry: retryDuration,
|
|
181
|
+
type: "welcome",
|
|
182
|
+
data: new Date().toLocaleTimeString(),
|
|
183
|
+
};
|
|
184
|
+
addEventToHistory(welcomeEvent);
|
|
185
|
+
|
|
186
|
+
// send to everyone
|
|
187
|
+
if (welcomeEventPublic) {
|
|
188
|
+
sendEventToAllClients(welcomeEvent, {
|
|
189
|
+
history: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// send only to this client
|
|
193
|
+
else {
|
|
194
|
+
client.sendEvent(welcomeEvent);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
const firstEvent = {
|
|
198
|
+
retry: retryDuration,
|
|
199
|
+
type: "comment",
|
|
200
|
+
data: new Date().toLocaleTimeString(),
|
|
201
|
+
};
|
|
202
|
+
client.sendEvent(firstEvent);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const removeClient = (client) => {
|
|
206
|
+
const index = clientArray.indexOf(client);
|
|
207
|
+
if (index === -1) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
clientArray.splice(index, 1);
|
|
211
|
+
logger.debug(`A client left. Number of client: ${clientArray.length}`);
|
|
212
|
+
if (clientArray.length === 0) {
|
|
213
|
+
if (typeof producerReturnValue === "function") {
|
|
214
|
+
producerReturnValue();
|
|
215
|
+
producerReturnValue = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const fetch = (request) => {
|
|
221
|
+
const isWebsocketUpgradeRequest =
|
|
222
|
+
request.headers["upgrade"] === "websocket";
|
|
223
|
+
const isEventSourceRequest = isWebsocketUpgradeRequest
|
|
224
|
+
? false
|
|
225
|
+
: request.headers["accept"] &&
|
|
226
|
+
request.headers["accept"].includes("text/event-stream");
|
|
227
|
+
if (!isWebsocketUpgradeRequest && !isEventSourceRequest) {
|
|
228
|
+
return {
|
|
229
|
+
status: 400,
|
|
230
|
+
body: "Bad Request, this endpoint only accepts WebSocket or EventSource requests",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (clientArray.length >= maxClientAllowed) {
|
|
235
|
+
if (actionOnClientLimitReached === "refuse") {
|
|
236
|
+
return {
|
|
237
|
+
status: 503,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// "kick-oldest"
|
|
241
|
+
const oldestClient = clientArray.shift();
|
|
242
|
+
oldestClient.close();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (serverEventSource.closed) {
|
|
246
|
+
return {
|
|
247
|
+
status: 204,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lastKnownId =
|
|
252
|
+
request.headers["last-event-id"] ||
|
|
253
|
+
request.searchParams.get("last-event-id");
|
|
254
|
+
if (isWebsocketUpgradeRequest) {
|
|
255
|
+
return new WebSocketResponse((websocket) => {
|
|
256
|
+
const webSocketClient = {
|
|
257
|
+
type: "websocket",
|
|
258
|
+
lastKnownId,
|
|
259
|
+
request,
|
|
260
|
+
websocket,
|
|
261
|
+
sendEvent: (event) => {
|
|
262
|
+
websocket.send(JSON.stringify(event));
|
|
263
|
+
},
|
|
264
|
+
close: () => {
|
|
265
|
+
websocket.close();
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
addClient(webSocketClient);
|
|
269
|
+
return () => {
|
|
270
|
+
removeClient(webSocketClient);
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// event source request
|
|
275
|
+
return {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: {
|
|
278
|
+
"content-type": "text/event-stream",
|
|
279
|
+
"cache-control": "no-store",
|
|
280
|
+
"connection": "keep-alive",
|
|
281
|
+
},
|
|
282
|
+
body: createObservable(({ next, complete, addTeardown }) => {
|
|
283
|
+
const client = {
|
|
284
|
+
type: "event_source",
|
|
285
|
+
lastKnownId,
|
|
286
|
+
request,
|
|
287
|
+
sendEvent: (event) => {
|
|
288
|
+
next(stringifySourceEvent(event));
|
|
289
|
+
},
|
|
290
|
+
close: () => {
|
|
291
|
+
complete(); // will terminate the http connection as body ends
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
addClient(client);
|
|
295
|
+
addTeardown(() => {
|
|
296
|
+
removeClient(client);
|
|
297
|
+
});
|
|
298
|
+
}),
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const addEventToHistory = (event) => {
|
|
303
|
+
if (typeof event.id === "undefined") {
|
|
304
|
+
event.id = computeEventId(event, previousEventId);
|
|
305
|
+
}
|
|
306
|
+
previousEventId = event.id;
|
|
307
|
+
eventHistory.add(event);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const sendEventToAllClients = (event, { history = true } = {}) => {
|
|
311
|
+
if (history) {
|
|
312
|
+
addEventToHistory(event);
|
|
313
|
+
}
|
|
314
|
+
logger.debug(`send "${event.type}" event to ${clientArray.size} client(s)`);
|
|
315
|
+
for (const client of clientArray) {
|
|
316
|
+
client.sendEvent(event);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const getAllEventSince = (id) => {
|
|
321
|
+
const events = eventHistory.since(id);
|
|
322
|
+
if (welcomeEventEnabled && !welcomeEventPublic) {
|
|
323
|
+
return events.filter((event) => event.type !== "welcome");
|
|
324
|
+
}
|
|
325
|
+
return events;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const keepAlive = () => {
|
|
329
|
+
// maybe that, when an event occurs, we can delay the keep alive event
|
|
330
|
+
logger.debug(
|
|
331
|
+
`send keep alive event, number of client listening this event source: ${clientArray.length}`,
|
|
332
|
+
);
|
|
333
|
+
sendEventToAllClients(
|
|
334
|
+
{
|
|
335
|
+
type: "comment",
|
|
336
|
+
data: new Date().toLocaleTimeString(),
|
|
337
|
+
},
|
|
338
|
+
{ history: false },
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const open = () => {
|
|
343
|
+
if (!serverEventSource.closed) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
interval = setInterval(keepAlive, keepaliveDuration);
|
|
347
|
+
if (!keepProcessAlive) {
|
|
348
|
+
interval.unref();
|
|
349
|
+
}
|
|
350
|
+
serverEventSource.closed = false;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const close = () => {
|
|
354
|
+
if (serverEventSource.closed) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
logger.debug(`closing, number of client : ${clientArray.length}`);
|
|
358
|
+
for (const client of clientArray) {
|
|
359
|
+
client.close();
|
|
360
|
+
}
|
|
361
|
+
clientArray.length = 0;
|
|
362
|
+
clearInterval(interval);
|
|
363
|
+
eventHistory.reset();
|
|
364
|
+
serverEventSource.closed = true;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
Object.assign(serverEventSource, {
|
|
368
|
+
// main api:
|
|
369
|
+
// - ability to sendEvent to clients in the room
|
|
370
|
+
// - ability to join the room
|
|
371
|
+
// - ability to leave the room
|
|
372
|
+
sendEventToAllClients,
|
|
373
|
+
fetch,
|
|
374
|
+
|
|
375
|
+
// should rarely be necessary, get information about the room
|
|
376
|
+
getAllEventSince,
|
|
377
|
+
getClientCount: () => clientArray.length,
|
|
378
|
+
|
|
379
|
+
// should rarely be used
|
|
380
|
+
close,
|
|
381
|
+
open,
|
|
382
|
+
});
|
|
383
|
+
return serverEventSource;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// https://github.com/dmail-old/project/commit/da7d2c88fc8273850812972885d030a22f9d7448
|
|
387
|
+
// https://github.com/dmail-old/project/commit/98b3ae6748d461ac4bd9c48944a551b1128f4459
|
|
388
|
+
// https://github.com/dmail-old/http-eventsource/blob/master/lib/event-source.js
|
|
389
|
+
// http://html5doctor.com/server-sent-events/
|
|
390
|
+
const stringifySourceEvent = ({ data, type = "message", id, retry }) => {
|
|
391
|
+
let string = "";
|
|
392
|
+
|
|
393
|
+
if (id !== undefined) {
|
|
394
|
+
string += `id:${id}\n`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (retry) {
|
|
398
|
+
string += `retry:${retry}\n`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (type !== "message") {
|
|
402
|
+
string += `event:${type}\n`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
string += `data:${data}\n\n`;
|
|
406
|
+
|
|
407
|
+
return string;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const createEventHistory = (limit) => {
|
|
411
|
+
const events = [];
|
|
412
|
+
|
|
413
|
+
const add = (data) => {
|
|
414
|
+
events.push(data);
|
|
415
|
+
|
|
416
|
+
if (events.length >= limit) {
|
|
417
|
+
events.shift();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const since = (id) => {
|
|
422
|
+
const index = events.findIndex((event) => String(event.id) === id);
|
|
423
|
+
return index === -1 ? [] : events.slice(index + 1);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const reset = () => {
|
|
427
|
+
events.length = 0;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return { add, since, reset };
|
|
431
|
+
};
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import { timeStart } from "./server_timing/timing_measure.js";
|
|
2
|
-
|
|
3
1
|
const HOOK_NAMES = [
|
|
4
2
|
"serverListening",
|
|
5
3
|
"redirectRequest",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
4
|
+
"augmentRouteFetchSecondArg",
|
|
5
|
+
"routes",
|
|
8
6
|
"handleError",
|
|
9
7
|
"onResponsePush",
|
|
10
|
-
"
|
|
11
|
-
"responseReady",
|
|
8
|
+
"injectResponseProperties",
|
|
12
9
|
"serverStopped",
|
|
13
10
|
];
|
|
14
11
|
|
|
15
12
|
export const createServiceController = (services) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
13
|
+
const hookSetMap = new Map();
|
|
14
|
+
|
|
15
|
+
const addHook = (hook) => {
|
|
16
|
+
let hookSet = hookSetMap.get(hook.name);
|
|
17
|
+
if (!hookSet) {
|
|
18
|
+
hookSet = new Set();
|
|
19
|
+
hookSetMap.set(hook.name, hookSet);
|
|
20
|
+
}
|
|
21
|
+
hookSet.add(hook);
|
|
22
|
+
};
|
|
18
23
|
|
|
19
24
|
const addService = (service) => {
|
|
20
|
-
Object.keys(service)
|
|
21
|
-
if (key === "name")
|
|
25
|
+
for (const key of Object.keys(service)) {
|
|
26
|
+
if (key === "name") continue;
|
|
22
27
|
const isHook = HOOK_NAMES.includes(key);
|
|
23
28
|
if (!isHook) {
|
|
24
29
|
console.warn(
|
|
@@ -27,73 +32,60 @@ export const createServiceController = (services) => {
|
|
|
27
32
|
}
|
|
28
33
|
const hookName = key;
|
|
29
34
|
const hookValue = service[hookName];
|
|
30
|
-
if (hookValue) {
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (!hookValue) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (hookName === "routes") {
|
|
39
|
+
} else {
|
|
40
|
+
addHook({
|
|
33
41
|
service,
|
|
34
42
|
name: hookName,
|
|
35
43
|
value: hookValue,
|
|
36
44
|
});
|
|
37
45
|
}
|
|
38
|
-
}
|
|
46
|
+
}
|
|
39
47
|
};
|
|
40
|
-
|
|
48
|
+
|
|
49
|
+
for (const service of services) {
|
|
41
50
|
addService(service);
|
|
42
|
-
}
|
|
51
|
+
}
|
|
43
52
|
|
|
44
53
|
let currentService = null;
|
|
45
54
|
let currentHookName = null;
|
|
46
55
|
const callHook = (hook, info, context) => {
|
|
47
|
-
const hookFn = hook
|
|
56
|
+
const hookFn = getHookFunction(hook, info);
|
|
48
57
|
if (!hookFn) {
|
|
49
58
|
return null;
|
|
50
59
|
}
|
|
51
60
|
currentService = hook.service;
|
|
52
61
|
currentHookName = hook.name;
|
|
53
|
-
let timeEnd;
|
|
54
|
-
if (context && context.timing) {
|
|
55
|
-
timeEnd = timeStart(
|
|
56
|
-
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
62
|
let valueReturned = hookFn(info, context);
|
|
60
|
-
if (context && context.timing) {
|
|
61
|
-
Object.assign(context.timing, timeEnd());
|
|
62
|
-
}
|
|
63
63
|
currentService = null;
|
|
64
64
|
currentHookName = null;
|
|
65
65
|
return valueReturned;
|
|
66
66
|
};
|
|
67
67
|
const callAsyncHook = async (hook, info, context) => {
|
|
68
|
-
const hookFn = hook
|
|
68
|
+
const hookFn = getHookFunction(hook, info);
|
|
69
69
|
if (!hookFn) {
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
currentService = hook.service;
|
|
73
73
|
currentHookName = hook.name;
|
|
74
|
-
let timeEnd;
|
|
75
|
-
if (context && context.timing) {
|
|
76
|
-
timeEnd = timeStart(
|
|
77
|
-
`${currentService.name.replace("jsenv:", "")}.${currentHookName}`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
74
|
let valueReturned = await hookFn(info, context);
|
|
81
|
-
if (context && context.timing) {
|
|
82
|
-
Object.assign(context.timing, timeEnd());
|
|
83
|
-
}
|
|
84
75
|
currentService = null;
|
|
85
76
|
currentHookName = null;
|
|
86
77
|
return valueReturned;
|
|
87
78
|
};
|
|
88
79
|
|
|
89
80
|
const callHooks = (hookName, info, context, callback = () => {}) => {
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
const hookSet = hookSetMap.get(hookName);
|
|
82
|
+
if (!hookSet) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const hook of hookSet) {
|
|
86
|
+
const returnValue = callHook(hook, info, context);
|
|
87
|
+
if (returnValue) {
|
|
88
|
+
callback(returnValue);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
91
|
};
|
|
@@ -103,46 +95,47 @@ export const createServiceController = (services) => {
|
|
|
103
95
|
context,
|
|
104
96
|
until = (returnValue) => returnValue,
|
|
105
97
|
) => {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
98
|
+
const hookSet = hookSetMap.get(hookName);
|
|
99
|
+
if (!hookSet) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
for (const hook of hookSet) {
|
|
103
|
+
const returnValue = callHook(hook, info, context);
|
|
104
|
+
const untilReturnValue = until(returnValue);
|
|
105
|
+
if (untilReturnValue) {
|
|
106
|
+
return untilReturnValue;
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
return null;
|
|
117
110
|
};
|
|
118
|
-
const callAsyncHooksUntil = (hookName, info, context) => {
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
111
|
+
const callAsyncHooksUntil = async (hookName, info, context) => {
|
|
112
|
+
const hookSet = hookSetMap.get(hookName);
|
|
113
|
+
if (!hookSet) {
|
|
121
114
|
return null;
|
|
122
115
|
}
|
|
123
|
-
if (
|
|
116
|
+
if (hookSet.size === 0) {
|
|
124
117
|
return null;
|
|
125
118
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
119
|
+
const iterator = hookSet.values()[Symbol.iterator]();
|
|
120
|
+
let result;
|
|
121
|
+
const visit = async () => {
|
|
122
|
+
const { done, value: hook } = iterator.next();
|
|
123
|
+
if (done) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const returnValue = await callAsyncHook(hook, info, context);
|
|
127
|
+
if (returnValue) {
|
|
128
|
+
result = returnValue;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await visit();
|
|
132
|
+
};
|
|
133
|
+
await visit();
|
|
134
|
+
return result;
|
|
142
135
|
};
|
|
143
136
|
|
|
144
137
|
return {
|
|
145
|
-
services
|
|
138
|
+
services,
|
|
146
139
|
|
|
147
140
|
callHooks,
|
|
148
141
|
callHooksUntil,
|
|
@@ -153,7 +146,20 @@ export const createServiceController = (services) => {
|
|
|
153
146
|
};
|
|
154
147
|
};
|
|
155
148
|
|
|
156
|
-
const
|
|
149
|
+
const getHookFunction = (hook, info) => {
|
|
150
|
+
const hookValue = hook.value;
|
|
151
|
+
if (hook.name === "handleRequest" && typeof hookValue === "object") {
|
|
152
|
+
const request = info;
|
|
153
|
+
const hookForMethod = hookValue[request.method] || hookValue["*"];
|
|
154
|
+
if (!hookForMethod) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return hookForMethod;
|
|
158
|
+
}
|
|
159
|
+
return hookValue;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const flattenAndFilterServices = (services) => {
|
|
157
163
|
const flatServices = [];
|
|
158
164
|
const visitServiceEntry = (serviceEntry) => {
|
|
159
165
|
if (Array.isArray(serviceEntry)) {
|