@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4

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 (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +22 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -0,0 +1,422 @@
1
+ import { MODULE_PROXY_PREFIXES } from "../proxy/rules.js";
2
+ // nodefony/src/profiler/aggregate.ts
3
+ /**
4
+ * Agrégation des profils de requêtes — règle PURE, sans horloge ni I/O.
5
+ *
6
+ * Une seule implémentation pour tout ce qui résume le profileur : l'outil MCP
7
+ * qui observe l'application (`nodefony_observe`), celui qui tire lui-même des
8
+ * requêtes sur une route, et demain la console d'administration. Elle est
9
+ * bornée par construction : un échantillon de durées par route, un nombre
10
+ * maximal de formes SQL retenues, quelques exemples d'erreurs — observer une
11
+ * application chargée ne fait jamais grossir la mémoire avec la fenêtre.
12
+ */
13
+ /**
14
+ * Répétitions d'une même forme SQL, dans UNE requête, à partir desquelles on
15
+ * signale un N+1. En dessous, ce sont des boucles courtes légitimes (quelques
16
+ * lectures de configuration, une pagination) qu'on signalerait à tort.
17
+ */
18
+ const N_PLUS_ONE_MIN_REPEATS = 5;
19
+ /** Durées retenues par route pour les percentiles (échantillonnage au-delà). */
20
+ const ROUTE_SAMPLE_CAP = 256;
21
+ /** Formes SQL distinctes retenues ; les suivantes ne sont plus que comptées. */
22
+ const SQL_SHAPE_CAP = 200;
23
+ /** Exemples d'erreurs retenus (statut ≥ 500 ou exception). */
24
+ const ERROR_SAMPLE_CAP = 10;
25
+ /** Préfixe des routes de l'outillage (console, API d'administration, porte MCP). */
26
+ const TOOLING_URL_PREFIX = "/nodefony/";
27
+ /**
28
+ * Range un profil par provenance, d'après son URL. Seule règle du partage —
29
+ * les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
30
+ *
31
+ * @param url - URL du profil, absolue ou chemin.
32
+ * @returns la provenance du profil.
33
+ */
34
+ function profileLane(url) {
35
+ const path = urlPath(url);
36
+ if (path.startsWith("/nodefony/")) return "tooling";
37
+ for (const prefix of MODULE_PROXY_PREFIXES) if (path.startsWith(prefix)) return "asset";
38
+ return "app";
39
+ }
40
+ /**
41
+ * Le profil vient-il de l'outillage (routes `/nodefony/…` ou assets relayés)
42
+ * plutôt que de l'application.
43
+ *
44
+ * @param url - URL du profil, absolue ou chemin.
45
+ */
46
+ function isToolingUrl(url) {
47
+ return profileLane(url) !== "app";
48
+ }
49
+ /** Groupe des requêtes qu'aucune route n'a servies (404, statique, sondes). */
50
+ const NO_ROUTE = "(aucune route)";
51
+ /** Pseudo-phase : le temps de l'action hors phases imbriquées et hors SQL. */
52
+ const ACTION_SELF_PHASE = "action.self";
53
+ /** Longueur maximale d'un message d'erreur rapporté. */
54
+ const ERROR_MESSAGE_MAX = 200;
55
+ /**
56
+ * Réduit une requête SQL (ou le descripteur d'une commande NoSQL) à sa FORME :
57
+ * valeurs remplacées par `?`, listes de valeurs repliées, blancs compactés.
58
+ *
59
+ * Deux requêtes qui ne diffèrent que par leurs valeurs ont la même forme — c'est
60
+ * ce qui permet de reconnaître un N+1. Et aucune valeur ne survit : la forme
61
+ * peut sortir dans un rapport sans emporter une donnée personnelle.
62
+ *
63
+ * @param sql - le texte tel que l'adaptateur l'a poussé au profileur.
64
+ * @returns la forme normalisée.
65
+ */
66
+ function sqlShape(sql) {
67
+ return sql.replace(/:\s*"(?:[^"\\]|\\.)*"/g, ":?").replace(/:\s*(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)\b/g, ":?").replace(/\[[^[\]{}]+\]/g, "[?…]").replace(/'(?:[^']|'')*'/g, "?").replace(/\$\d+/g, "?").replace(/(?<![\w$])\d+(?:\.\d+)?/g, "?").replace(/\bin\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/gi, "in (?…)").replace(/\bvalues\s*\([^()]*\)(?:\s*,\s*\([^()]*\))*/gi, "values (…)").replace(/\s+/g, " ").trim();
68
+ }
69
+ /** Phase `action` terminée : sa durée est connue. */
70
+ function isFinishedAction(p) {
71
+ return p.name === "action" && p.durationMs !== null;
72
+ }
73
+ /**
74
+ * Temps de l'action hors phases imbriquées et hors SQL : le code de l'action
75
+ * elle-même et des services qu'elle appelle.
76
+ *
77
+ * Les phases imbriquées (`initialize`, `render`, `send`…) ne sont pas
78
+ * nommées ici : est imbriquée toute phase terminée qui tient dans l'intervalle
79
+ * de l'action. Les intervalles (phases et SQL) sont fusionnés avant d'être
80
+ * retranchés — une requête SQL lancée pendant le rendu n'est pas comptée deux
81
+ * fois.
82
+ *
83
+ * @returns la durée propre en ms, ou `null` sans phase `action` terminée.
84
+ */
85
+ function actionSelfMs(entry) {
86
+ const action = entry.phases.find(isFinishedAction);
87
+ if (action === void 0) return null;
88
+ const start = action.startMs;
89
+ const end = start + action.durationMs;
90
+ const inner = [];
91
+ for (const p of entry.phases) {
92
+ if (p === action || p.durationMs === null) continue;
93
+ const pEnd = p.startMs + p.durationMs;
94
+ if (p.startMs >= start && pEnd <= end) inner.push([p.startMs, pEnd]);
95
+ }
96
+ for (const q of entry.queries ?? []) {
97
+ if (q.startMs === void 0) continue;
98
+ const qStart = Math.max(q.startMs, start);
99
+ const qEnd = Math.min(q.startMs + q.durationMs, end);
100
+ if (qEnd > qStart) inner.push([qStart, qEnd]);
101
+ }
102
+ inner.sort((a, b) => a[0] - b[0]);
103
+ let covered = 0;
104
+ let curStart = -Infinity;
105
+ let curEnd = -Infinity;
106
+ for (const [s, e] of inner) if (s > curEnd) {
107
+ if (curEnd > curStart) covered += curEnd - curStart;
108
+ curStart = s;
109
+ curEnd = e;
110
+ } else if (e > curEnd) curEnd = e;
111
+ if (curEnd > curStart) covered += curEnd - curStart;
112
+ return Math.max(0, action.durationMs - covered);
113
+ }
114
+ /**
115
+ * Une requête en ERREUR du serveur : statut ≥ 500, ou exception levée sans
116
+ * verdict client. Règle unique — le rapport, ses exemples et la lecture de leurs
117
+ * journaux en dépendent.
118
+ *
119
+ * Un statut 4xx n'en est JAMAIS une, même porteur d'une exception : un refus
120
+ * 401/403 (l'authentification lève une `AuthenticationError`) ou une ressource
121
+ * absente est le verdict rendu au client, compté à part — le lister parmi les
122
+ * erreurs ferait chercher une panne là où il manque une identité.
123
+ *
124
+ * @param entry - le profil (seuls statut et erreur sont lus).
125
+ */
126
+ function isFailedProfile(entry) {
127
+ if (entry.status === null) return entry.error !== null;
128
+ if (entry.status >= 500) return true;
129
+ return entry.status < 400 && entry.error !== null;
130
+ }
131
+ /**
132
+ * Le chemin d'une URL de profil. Le profileur enregistre l'URL ABSOLUE
133
+ * (`http://hôte:port/chemin`) : un filtre écrit sur le chemin ne mordrait
134
+ * jamais dessus. Sans `URL` à construire — appelée sur chaque profil lu.
135
+ */
136
+ function urlPath(url) {
137
+ if (url.startsWith("/")) return url;
138
+ const scheme = url.indexOf("://");
139
+ if (scheme === -1) return url;
140
+ const slash = url.indexOf("/", scheme + 3);
141
+ return slash === -1 ? "/" : url.slice(slash);
142
+ }
143
+ /** Percentile au rang le plus proche, sur un tableau TRIÉ non vide. */
144
+ function percentile(sorted, p) {
145
+ if (sorted.length === 0) return null;
146
+ const rank = Math.ceil(p / 100 * sorted.length) - 1;
147
+ return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)] ?? null;
148
+ }
149
+ /** Arrondi au centième de milliseconde : au-delà, c'est du bruit de mesure. */
150
+ function round(ms) {
151
+ return Math.round(ms * 100) / 100;
152
+ }
153
+ function emptyStatus() {
154
+ return {
155
+ s2xx: 0,
156
+ s3xx: 0,
157
+ s4xx: 0,
158
+ s5xx: 0,
159
+ unauthorized: 0,
160
+ forbidden: 0
161
+ };
162
+ }
163
+ function countStatus(status, code) {
164
+ if (code === null) return;
165
+ if (code >= 500) status.s5xx += 1;
166
+ else if (code >= 400) {
167
+ status.s4xx += 1;
168
+ if (code === 401) status.unauthorized += 1;
169
+ else if (code === 403) status.forbidden += 1;
170
+ } else if (code >= 300) status.s3xx += 1;
171
+ else if (code >= 200) status.s2xx += 1;
172
+ }
173
+ function addPhase(phases, name, ms) {
174
+ const total = phases[name] ??= {
175
+ totalMs: 0,
176
+ count: 0
177
+ };
178
+ total.totalMs += ms;
179
+ total.count += 1;
180
+ }
181
+ /**
182
+ * Crée un accumulateur de profils.
183
+ *
184
+ * @param options - seuil N+1, prise en compte de l'outillage, aléa injecté.
185
+ * @returns l'accumulateur ; son `result()` peut être appelé à tout moment.
186
+ */
187
+ function createProfileAggregator(options = {}) {
188
+ const minRepeats = options.nPlusOneMinRepeats ?? 5;
189
+ const includeTooling = options.includeTooling ?? false;
190
+ const random = options.random ?? Math.random;
191
+ const routes = /* @__PURE__ */ new Map();
192
+ const nPlusOne = /* @__PURE__ */ new Map();
193
+ const shapes = /* @__PURE__ */ new Map();
194
+ const errors = [];
195
+ let requests = 0;
196
+ let http = 0;
197
+ let ws = 0;
198
+ let filteredTooling = 0;
199
+ let untimed = 0;
200
+ let sqlShapesDropped = 0;
201
+ const droppedShapes = /* @__PURE__ */ new Set();
202
+ function routeAcc(entry, route) {
203
+ const key = `${entry.kind}\u0000${route}`;
204
+ let acc = routes.get(key);
205
+ if (acc === void 0) {
206
+ acc = {
207
+ stats: {
208
+ route,
209
+ kind: entry.kind,
210
+ controller: entry.controller,
211
+ action: entry.action,
212
+ count: 0,
213
+ untimed: 0,
214
+ served: 0,
215
+ p50Ms: null,
216
+ p95Ms: null,
217
+ maxMs: null,
218
+ status: emptyStatus(),
219
+ errors: 0,
220
+ queries: {
221
+ total: 0,
222
+ perRequestMax: 0,
223
+ totalMs: 0
224
+ },
225
+ phases: {}
226
+ },
227
+ samples: [],
228
+ seen: 0
229
+ };
230
+ routes.set(key, acc);
231
+ }
232
+ return acc;
233
+ }
234
+ function sample(acc, ms) {
235
+ acc.seen += 1;
236
+ if (acc.samples.length < 256) {
237
+ acc.samples.push(ms);
238
+ return;
239
+ }
240
+ const slot = Math.floor(random() * acc.seen);
241
+ if (slot < 256) acc.samples[slot] = ms;
242
+ }
243
+ function countQueries(entry, stats) {
244
+ const queries = entry.queries;
245
+ if (queries === void 0 || queries.length === 0) return;
246
+ const q = stats.queries;
247
+ q.total += queries.length;
248
+ if (queries.length > q.perRequestMax) q.perRequestMax = queries.length;
249
+ for (const one of queries) q.totalMs += one.durationMs;
250
+ }
251
+ function addQueries(entry, route) {
252
+ const queries = entry.queries;
253
+ if (queries === void 0 || queries.length === 0) return;
254
+ const perShape = /* @__PURE__ */ new Map();
255
+ for (const q of queries) {
256
+ const shape = sqlShape(q.sql);
257
+ const local = perShape.get(shape);
258
+ if (local === void 0) perShape.set(shape, {
259
+ n: 1,
260
+ ms: q.durationMs
261
+ });
262
+ else {
263
+ local.n += 1;
264
+ local.ms += q.durationMs;
265
+ }
266
+ let global = shapes.get(shape);
267
+ if (global === void 0) {
268
+ if (shapes.size >= 200) {
269
+ if (!droppedShapes.has(shape) && droppedShapes.size < 200) droppedShapes.add(shape);
270
+ continue;
271
+ }
272
+ global = {
273
+ shape,
274
+ connector: q.connector ?? null,
275
+ count: 0,
276
+ totalMs: 0,
277
+ maxMs: 0,
278
+ routes: /* @__PURE__ */ new Set()
279
+ };
280
+ shapes.set(shape, global);
281
+ }
282
+ global.count += 1;
283
+ global.totalMs += q.durationMs;
284
+ if (q.durationMs > global.maxMs) global.maxMs = q.durationMs;
285
+ if (global.routes.size < 5) global.routes.add(route);
286
+ }
287
+ for (const [shape, { n, ms }] of perShape) {
288
+ if (n < minRepeats) continue;
289
+ const key = `${route}\u0000${shape}`;
290
+ const found = nPlusOne.get(key);
291
+ if (found === void 0) nPlusOne.set(key, {
292
+ route,
293
+ controller: entry.controller,
294
+ action: entry.action,
295
+ shape,
296
+ maxRepeats: n,
297
+ requests: 1,
298
+ totalMs: ms,
299
+ sampleRequestId: entry.requestId
300
+ });
301
+ else {
302
+ found.requests += 1;
303
+ found.totalMs += ms;
304
+ if (n > found.maxRepeats) {
305
+ found.maxRepeats = n;
306
+ found.sampleRequestId = entry.requestId;
307
+ }
308
+ }
309
+ }
310
+ }
311
+ return {
312
+ add(entry) {
313
+ if (!includeTooling && isToolingUrl(entry.url)) {
314
+ filteredTooling += 1;
315
+ return false;
316
+ }
317
+ requests += 1;
318
+ if (entry.kind === "ws") ws += 1;
319
+ else http += 1;
320
+ const route = entry.route ?? "(aucune route)";
321
+ const acc = routeAcc(entry, route);
322
+ const stats = acc.stats;
323
+ stats.count += 1;
324
+ countStatus(stats.status, entry.status);
325
+ const failed = isFailedProfile(entry);
326
+ if (failed) {
327
+ stats.errors += 1;
328
+ if (errors.length < 10) errors.push({
329
+ route,
330
+ status: entry.status,
331
+ error: entry.error === null ? null : entry.error.slice(0, ERROR_MESSAGE_MAX),
332
+ requestId: entry.requestId
333
+ });
334
+ }
335
+ const served = !failed && (entry.status === null || entry.status < 400);
336
+ if (entry.durationMs === null) {
337
+ stats.untimed += 1;
338
+ untimed += 1;
339
+ } else if (served) {
340
+ stats.served += 1;
341
+ sample(acc, entry.durationMs);
342
+ if (stats.maxMs === null || entry.durationMs > stats.maxMs) stats.maxMs = entry.durationMs;
343
+ for (const p of entry.phases) if (p.durationMs !== null) addPhase(stats.phases, p.name, p.durationMs);
344
+ const self = actionSelfMs(entry);
345
+ if (self !== null) addPhase(stats.phases, ACTION_SELF_PHASE, self);
346
+ countQueries(entry, stats);
347
+ }
348
+ addQueries(entry, route);
349
+ sqlShapesDropped = droppedShapes.size;
350
+ return true;
351
+ },
352
+ result() {
353
+ const routeList = [];
354
+ for (const acc of routes.values()) {
355
+ const sorted = [...acc.samples].sort((a, b) => a - b);
356
+ const p50 = percentile(sorted, 50);
357
+ const p95 = percentile(sorted, 95);
358
+ const phases = {};
359
+ for (const [name, t] of Object.entries(acc.stats.phases)) phases[name] = {
360
+ totalMs: round(t.totalMs),
361
+ count: t.count
362
+ };
363
+ routeList.push({
364
+ ...acc.stats,
365
+ p50Ms: p50 === null ? null : round(p50),
366
+ p95Ms: p95 === null ? null : round(p95),
367
+ maxMs: acc.stats.maxMs === null ? null : round(acc.stats.maxMs),
368
+ status: { ...acc.stats.status },
369
+ queries: {
370
+ ...acc.stats.queries,
371
+ totalMs: round(acc.stats.queries.totalMs)
372
+ },
373
+ phases
374
+ });
375
+ }
376
+ routeList.sort((a, b) => (b.p95Ms ?? -1) - (a.p95Ms ?? -1));
377
+ const nPlusOneList = [...nPlusOne.values()].map((n) => ({
378
+ route: n.route,
379
+ controller: n.controller,
380
+ action: n.action,
381
+ shape: n.shape,
382
+ maxRepeats: n.maxRepeats,
383
+ requests: n.requests,
384
+ totalMs: round(n.totalMs),
385
+ sampleRequestId: n.sampleRequestId
386
+ })).sort((a, b) => b.totalMs - a.totalMs);
387
+ const slowQueries = [...shapes.values()].sort((a, b) => b.totalMs - a.totalMs).slice(0, 10).map((s) => ({
388
+ shape: s.shape,
389
+ connector: s.connector,
390
+ count: s.count,
391
+ totalMs: round(s.totalMs),
392
+ maxMs: round(s.maxMs),
393
+ routes: [...s.routes]
394
+ }));
395
+ return {
396
+ requests,
397
+ http,
398
+ ws,
399
+ filteredTooling,
400
+ untimed,
401
+ routes: routeList,
402
+ nPlusOne: nPlusOneList,
403
+ slowQueries,
404
+ sqlShapesDropped,
405
+ errors: errors.slice()
406
+ };
407
+ }
408
+ };
409
+ }
410
+ /**
411
+ * Agrège une liste de profils en une fois.
412
+ *
413
+ * @param entries - les profils, dans n'importe quel ordre.
414
+ * @param options - mêmes réglages que {@link createProfileAggregator}.
415
+ * @returns le rapport.
416
+ */
417
+ function aggregateProfiles(entries, options = {}) {
418
+ const aggregator = createProfileAggregator(options);
419
+ for (const entry of entries) aggregator.add(entry);
420
+ return aggregator.result();
421
+ }
422
+ export { ACTION_SELF_PHASE, ERROR_SAMPLE_CAP, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, ROUTE_SAMPLE_CAP, SQL_SHAPE_CAP, TOOLING_URL_PREFIX, aggregateProfiles, createProfileAggregator, isFailedProfile, isToolingUrl, profileLane, sqlShape, urlPath };