@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.
Files changed (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +19 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. 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
- "handleRequest",
7
- "handleWebsocket",
4
+ "augmentRouteFetchSecondArg",
5
+ "routes",
8
6
  "handleError",
9
7
  "onResponsePush",
10
- "injectResponseHeaders",
11
- "responseReady",
8
+ "injectResponseProperties",
12
9
  "serverStopped",
13
10
  ];
14
11
 
15
12
  export const createServiceController = (services) => {
16
- const flatServices = flattenAndFilterServices(services);
17
- const hookGroups = {};
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).forEach((key) => {
21
- if (key === "name") return;
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
- const group = hookGroups[hookName] || (hookGroups[hookName] = []);
32
- group.push({
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
- flatServices.forEach((service) => {
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.value;
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.value;
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 hooks = hookGroups[hookName];
91
- if (hooks) {
92
- for (const hook of hooks) {
93
- const returnValue = callHook(hook, info, context);
94
- if (returnValue) {
95
- callback(returnValue);
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 hooks = hookGroups[hookName];
107
- if (hooks) {
108
- for (const hook of hooks) {
109
- const returnValue = callHook(hook, info, context);
110
- const untilReturnValue = until(returnValue);
111
- if (untilReturnValue) {
112
- return untilReturnValue;
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 hooks = hookGroups[hookName];
120
- if (!hooks) {
111
+ const callAsyncHooksUntil = async (hookName, info, context) => {
112
+ const hookSet = hookSetMap.get(hookName);
113
+ if (!hookSet) {
121
114
  return null;
122
115
  }
123
- if (hooks.length === 0) {
116
+ if (hookSet.size === 0) {
124
117
  return null;
125
118
  }
126
- return new Promise((resolve, reject) => {
127
- const visit = (index) => {
128
- if (index >= hooks.length) {
129
- return resolve();
130
- }
131
- const hook = hooks[index];
132
- const returnValue = callAsyncHook(hook, info, context);
133
- return Promise.resolve(returnValue).then((output) => {
134
- if (output) {
135
- return resolve(output);
136
- }
137
- return visit(index + 1);
138
- }, reject);
139
- };
140
- visit(0);
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: flatServices,
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 flattenAndFilterServices = (services) => {
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)) {