@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.5
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/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +13 -10
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +3 -3
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
- package/dist/nodefony/service/audit-logger.js +23 -30
- package/dist/nodefony/service/certificates.js +97 -13
- package/dist/nodefony/service/error-renderer.js +29 -12
- package/dist/nodefony/service/http-kernel.js +198 -50
- package/dist/nodefony/service/pretty-request-logger.js +15 -13
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +3 -3
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
- package/dist/nodefony/service/servers/server-websocket.js +10 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
- package/dist/nodefony/service/sessions/sessions-service.js +7 -14
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +78 -43
- package/dist/nodefony/src/context/domainMatcher.js +1 -2
- package/dist/nodefony/src/context/forwarded.js +1 -2
- package/dist/nodefony/src/context/http/HttpContext.js +28 -6
- package/dist/nodefony/src/context/http/Request.js +33 -29
- package/dist/nodefony/src/context/http/Response.js +23 -6
- package/dist/nodefony/src/context/http/SseStream.js +1 -2
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +41 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/responseEnded.js +1 -2
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +33 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
- package/dist/nodefony/src/profiler/Profiler.js +104 -32
- package/dist/nodefony/src/profiler/aggregate.js +442 -0
- package/dist/nodefony/src/proxy/forward.js +29 -5
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +12 -6
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
- package/dist/nodefony/src/session/session.js +2 -3
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +9 -3
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/certificates.d.ts +55 -4
- package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/src/context/Context.d.ts +38 -5
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
- package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
- package/docs/cookies.md +85 -78
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +6 -1
- package/docs/observabilite.md +159 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +72 -70
- package/docs/servers.md +338 -224
- package/docs/session.md +179 -179
- package/docs/sse.md +24 -20
- package/docs/upload.md +120 -117
- package/package.json +8 -9
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
2
|
+
import { closeRequestTrace } from "../requestTrace.js";
|
|
2
3
|
import Context from "../Context.js";
|
|
3
4
|
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
4
5
|
import WebsocketResponse from "./Response.js";
|
|
@@ -6,11 +7,11 @@ import { sanitizeRequestId } from "../requestId.js";
|
|
|
6
7
|
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
7
8
|
import { formatWsLogContent } from "./wsLogContent.js";
|
|
8
9
|
import { parseWebsocketUrl } from "./websocketUrl.js";
|
|
9
|
-
import { RequestContext, logColor } from "nodefony";
|
|
10
|
-
import Ws from "ws";
|
|
10
|
+
import { RequestContext, frameRequestId, logColor } from "nodefony";
|
|
11
11
|
import { AsyncResource } from "node:async_hooks";
|
|
12
12
|
import url, { URL } from "node:url";
|
|
13
|
-
|
|
13
|
+
import Ws from "ws";
|
|
14
|
+
// nodefony/src/context/websocket/WebsocketContext.ts
|
|
14
15
|
/**
|
|
15
16
|
* Appelle l'action résolue et rend TOUJOURS une promesse.
|
|
16
17
|
*
|
|
@@ -54,6 +55,51 @@ function toWsCloseCode(code) {
|
|
|
54
55
|
return 1011;
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
58
|
+
* Statut HTTP d'un handshake REFUSÉ avant le `101` (RFC 6455 §4.2.2) — la
|
|
59
|
+
* réciproque de {@link toWsCloseCode} : une erreur levée pendant la décision
|
|
60
|
+
* porte un code HTTP (pare-feu, garde) ou, par héritage, un code de fermeture
|
|
61
|
+
* WS (origine `1008`, protocole `1002`, introuvable `4004`) ; le client qui
|
|
62
|
+
* lit le statut doit y reconnaître le même sens.
|
|
63
|
+
*
|
|
64
|
+
* @param code - code porté par l'erreur
|
|
65
|
+
* @returns un statut HTTP 4xx/5xx
|
|
66
|
+
*/
|
|
67
|
+
function toHandshakeStatus(code) {
|
|
68
|
+
if (typeof code !== "number" || !Number.isInteger(code)) return 500;
|
|
69
|
+
if (code >= 400 && code <= 599) return code;
|
|
70
|
+
switch (code) {
|
|
71
|
+
case 1002:
|
|
72
|
+
case 1003:
|
|
73
|
+
case 1007: return 400;
|
|
74
|
+
case 1008:
|
|
75
|
+
case 4003: return 403;
|
|
76
|
+
case 4001: return 401;
|
|
77
|
+
case 1009: return 413;
|
|
78
|
+
case 1013: return 503;
|
|
79
|
+
case 4004: return 404;
|
|
80
|
+
default: return 500;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Statut HTTP que reçoit le client d'un handshake refusé — ou `null` si la
|
|
85
|
+
* connexion était déjà acceptée.
|
|
86
|
+
*
|
|
87
|
+
* La ligne de journal d'un refus s'écrit AVANT `refuseHandshake` : sans ce
|
|
88
|
+
* calcul, elle affichait le code de fermeture par défaut (`1000`) en ERROR,
|
|
89
|
+
* pour un `400` que le client reçoit. Même conversion que le refus
|
|
90
|
+
* ({@link toHandshakeStatus}), donc même chiffre. Chemin d'ERREUR seulement :
|
|
91
|
+
* 1 comparaison et 1 `typeof`, aucune allocation.
|
|
92
|
+
*
|
|
93
|
+
* @param context - le contexte WS de la ligne
|
|
94
|
+
* @param error - l'erreur qui a interrompu le handshake
|
|
95
|
+
* @returns le statut HTTP du refus, ou `null` après l'acceptation
|
|
96
|
+
*/
|
|
97
|
+
function handshakeRefusalStatus(context, error) {
|
|
98
|
+
if (context.webSocketState !== "handshake") return null;
|
|
99
|
+
const code = error.code;
|
|
100
|
+
return toHandshakeStatus(typeof code === "number" ? code : void 0);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
57
103
|
* Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
|
|
58
104
|
* premier message (kernel présent). `true` hors production : le Suivi de requête
|
|
59
105
|
* (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
|
|
@@ -68,16 +114,21 @@ var WebsocketContext = class extends Context {
|
|
|
68
114
|
rejected = false;
|
|
69
115
|
teardownWired = false;
|
|
70
116
|
connection = null;
|
|
117
|
+
/**
|
|
118
|
+
* Le verdict du handshake encore à rendre : posé par le noyau tant que le
|
|
119
|
+
* pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
|
|
120
|
+
* est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
|
|
121
|
+
*/
|
|
122
|
+
admission = null;
|
|
71
123
|
origin;
|
|
72
124
|
proxy = null;
|
|
73
125
|
forwarded = null;
|
|
74
126
|
wsUrl = null;
|
|
75
|
-
|
|
76
|
-
queryRequest = {};
|
|
127
|
+
query = {};
|
|
77
128
|
wsPath = "";
|
|
78
129
|
#frameSeq = 0;
|
|
79
|
-
constructor(scope, req, ws,
|
|
80
|
-
super(scope,
|
|
130
|
+
constructor(scope, req, ws, serverType) {
|
|
131
|
+
super(scope, serverType);
|
|
81
132
|
this.webSocketState = "handshake";
|
|
82
133
|
this.request = req;
|
|
83
134
|
this.connection = ws;
|
|
@@ -89,18 +140,16 @@ var WebsocketContext = class extends Context {
|
|
|
89
140
|
if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
|
|
90
141
|
this.remoteAddress = this.getRemoteAddress();
|
|
91
142
|
this.acceptedProtocol = req.headers["sec-websocket-protocol"];
|
|
92
|
-
this.scheme =
|
|
143
|
+
this.scheme = serverType === "wss" ? "wss" : "ws";
|
|
93
144
|
const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
|
|
94
145
|
if (incomingId) this.requestId = incomingId;
|
|
95
146
|
this.wsUrl = parseWebsocketUrl(this.scheme, req.headers.host, req.url);
|
|
96
|
-
this.
|
|
97
|
-
this.queryRequest = { ...this.queryGet };
|
|
147
|
+
this.query = Object.fromEntries(this.wsUrl.searchParams.entries());
|
|
98
148
|
this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
|
|
99
149
|
this.url = url.format(this.wsUrl);
|
|
100
|
-
this.port = parseInt(this.wsUrl.port, 10) || (
|
|
150
|
+
this.port = parseInt(this.wsUrl.port, 10) || (serverType === "wss" ? 443 : 80);
|
|
101
151
|
this.request.url = this.wsUrl;
|
|
102
|
-
this.request.
|
|
103
|
-
this.request.query = this.queryRequest;
|
|
152
|
+
this.request.query = this.query;
|
|
104
153
|
this.request.path = this.wsPath;
|
|
105
154
|
try {
|
|
106
155
|
this.originUrl = new URL(this.origin);
|
|
@@ -125,22 +174,51 @@ var WebsocketContext = class extends Context {
|
|
|
125
174
|
this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy.proxyVia}`, "DEBUG");
|
|
126
175
|
}
|
|
127
176
|
}
|
|
128
|
-
log(pci, severity, msgid, msg) {
|
|
177
|
+
log(pci, severity, msgid, msg, moduleName) {
|
|
129
178
|
if (!msgid) msgid = "WEBSOCKET CONTEXT";
|
|
130
|
-
return super.log(pci, severity, msgid, msg);
|
|
179
|
+
return super.log(pci, severity, msgid, msg, moduleName);
|
|
131
180
|
}
|
|
132
181
|
logRequest(httpError, acceptedProtocol) {
|
|
133
182
|
try {
|
|
134
183
|
const logger = this.httpKernel?.getRequestLogger();
|
|
135
184
|
if (!logger) return;
|
|
136
185
|
const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
|
|
137
|
-
const pdu = this.log(entry.text, entry.severity, entry.msgid);
|
|
186
|
+
const pdu = this.log(entry.text, entry.severity, entry.msgid, void 0, this.logModule ?? this.resolver?.route?.module?.name);
|
|
138
187
|
pdu.requestId ??= this.requestId;
|
|
139
188
|
return pdu;
|
|
140
189
|
} catch {
|
|
141
190
|
return;
|
|
142
191
|
}
|
|
143
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
|
|
195
|
+
* contexte a été construit et jugé AVANT, sans elle.
|
|
196
|
+
*
|
|
197
|
+
* @param ws - la socket acceptée
|
|
198
|
+
*/
|
|
199
|
+
acceptConnection(ws) {
|
|
200
|
+
this.admission = null;
|
|
201
|
+
this.connection = ws;
|
|
202
|
+
this.response?.setConnection(ws);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Refuse le handshake par un statut HTTP, avant tout `101` (RFC 6455
|
|
206
|
+
* §4.2.2) — avec les en-têtes posés pendant la décision (`WWW-Authenticate`
|
|
207
|
+
* du pare-feu, `Retry-After`).
|
|
208
|
+
*
|
|
209
|
+
* @param status - statut HTTP du refus
|
|
210
|
+
* @returns `false` si le verdict était déjà rendu (rien n'est écrit)
|
|
211
|
+
*/
|
|
212
|
+
refuseHandshake(status) {
|
|
213
|
+
const admission = this.admission;
|
|
214
|
+
if (admission === null) return false;
|
|
215
|
+
this.admission = null;
|
|
216
|
+
this.rejected = true;
|
|
217
|
+
this.webSocketState = "error";
|
|
218
|
+
admission.refuse(status, this.response?.handshakeHeaders ?? null);
|
|
219
|
+
if (this.requestTrace !== null) closeRequestTrace(this, status, this.error);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
144
222
|
async connect() {
|
|
145
223
|
if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
|
|
146
224
|
if (this.rejected) throw new Error("Nodefony Websocket rejected");
|
|
@@ -152,6 +230,7 @@ var WebsocketContext = class extends Context {
|
|
|
152
230
|
this.requestEnded = true;
|
|
153
231
|
this.connection.on("message", AsyncResource.bind(this.handleMessage.bind(this)));
|
|
154
232
|
this.logRequest(null, this.acceptedProtocol ?? null);
|
|
233
|
+
if (this.requestTrace !== null) closeRequestTrace(this, 101, null);
|
|
155
234
|
this.webSocketState = "connected";
|
|
156
235
|
return this.connection;
|
|
157
236
|
}
|
|
@@ -261,10 +340,11 @@ var WebsocketContext = class extends Context {
|
|
|
261
340
|
* zéro allocation dans ce cas.
|
|
262
341
|
*/
|
|
263
342
|
beginFrame(method, framePath) {
|
|
264
|
-
|
|
343
|
+
const profiled = this.profiling && this.httpKernel?.shouldProfile(framePath) === true;
|
|
344
|
+
if (!profiled && !this.timingEnabled) return null;
|
|
265
345
|
this.#frameSeq += 1;
|
|
266
346
|
return new FrameProfile({
|
|
267
|
-
requestId:
|
|
347
|
+
requestId: frameRequestId(this.requestId, this.#frameSeq),
|
|
268
348
|
type: this.type,
|
|
269
349
|
scheme: this.scheme,
|
|
270
350
|
method,
|
|
@@ -274,7 +354,7 @@ var WebsocketContext = class extends Context {
|
|
|
274
354
|
security: this.security ?? null,
|
|
275
355
|
securityTrace: this.securityTrace,
|
|
276
356
|
timing: this.timingEnabled,
|
|
277
|
-
queries:
|
|
357
|
+
queries: profiled
|
|
278
358
|
});
|
|
279
359
|
}
|
|
280
360
|
/**
|
|
@@ -282,7 +362,7 @@ var WebsocketContext = class extends Context {
|
|
|
282
362
|
* Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
|
|
283
363
|
*/
|
|
284
364
|
collectFrame(frame) {
|
|
285
|
-
if (frame === null || !
|
|
365
|
+
if (frame === null || !frame.profiled) return;
|
|
286
366
|
this.get("profiler")?.collect(frame);
|
|
287
367
|
}
|
|
288
368
|
/**
|
|
@@ -337,12 +417,12 @@ var WebsocketContext = class extends Context {
|
|
|
337
417
|
}
|
|
338
418
|
onClose(code, reason) {
|
|
339
419
|
const description = reason.toString();
|
|
340
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.
|
|
420
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.serverType} ${logColor.magenta(code)} CLOSE ${this.method}`);
|
|
341
421
|
if (this.connection?.readyState !== Ws.CLOSED) {
|
|
342
422
|
try {
|
|
343
423
|
this.response?.drop(code, description);
|
|
344
424
|
} catch (e) {
|
|
345
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.
|
|
425
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.serverType} CLOSE ${logColor.red(this.method ?? "")}`);
|
|
346
426
|
}
|
|
347
427
|
this.fire("onClose", code, description, this.connection);
|
|
348
428
|
} else this.fire("onClose", code, description, this.connection);
|
|
@@ -359,7 +439,7 @@ var WebsocketContext = class extends Context {
|
|
|
359
439
|
*/
|
|
360
440
|
onConnectionError(error) {
|
|
361
441
|
this.webSocketState = "error";
|
|
362
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.
|
|
442
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.serverType} ${logColor.red("SOCKET ERROR")} ${this.method}`);
|
|
363
443
|
this.fire("onError", error, this);
|
|
364
444
|
}
|
|
365
445
|
setScheme() {
|
|
@@ -417,5 +497,4 @@ var WebsocketContext = class extends Context {
|
|
|
417
497
|
this.rejected = true;
|
|
418
498
|
}
|
|
419
499
|
};
|
|
420
|
-
|
|
421
|
-
export { WebsocketContext as default, toWsCloseCode };
|
|
500
|
+
export { WebsocketContext as default, handshakeRefusalStatus, toHandshakeStatus, toWsCloseCode };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// nodefony/src/context/websocket/isWebsocketContext.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dit si un contexte sert une connexion WebSocket — la garde qui rétrécit
|
|
4
|
+
* `ContextType` à `WebsocketContext` sans conversion.
|
|
5
|
+
*
|
|
6
|
+
* Elle lit la famille (`type`) posée à la construction du contexte, jamais
|
|
7
|
+
* `instanceof` : deux copies installées de `@nodefony/http` font échouer
|
|
8
|
+
* `instanceof` en silence, quand ce champ dit vrai pour les deux. Deux
|
|
9
|
+
* Une comparaison de chaînes, aucune allocation.
|
|
10
|
+
*
|
|
11
|
+
* @param context - le contexte à éprouver.
|
|
12
|
+
* @returns `true` si le contexte traite une connexion WebSocket (`ws` ou `wss`).
|
|
13
|
+
*/
|
|
14
|
+
function isWebsocketContext(context) {
|
|
15
|
+
return context.type === "websocket";
|
|
16
|
+
}
|
|
17
|
+
export { isWebsocketContext };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/websocketUrl.ts
|
|
2
2
|
/**
|
|
3
3
|
* URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
|
|
4
4
|
*
|
|
@@ -17,5 +17,4 @@
|
|
|
17
17
|
function parseWebsocketUrl(scheme, host, rawUrl) {
|
|
18
18
|
return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
|
|
19
19
|
}
|
|
20
|
-
//#endregion
|
|
21
20
|
export { parseWebsocketUrl };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsBackpressure.ts
|
|
2
2
|
/**
|
|
3
3
|
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
4
4
|
*
|
|
@@ -52,5 +52,4 @@ const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
|
|
|
52
52
|
}
|
|
53
53
|
return "drop";
|
|
54
54
|
};
|
|
55
|
-
//#endregion
|
|
56
55
|
export { decideSend, readBackpressureOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsCloseReason.ts
|
|
2
2
|
/**
|
|
3
3
|
* Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
|
|
4
4
|
* §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
|
|
@@ -37,5 +37,4 @@ function toWsCloseReason(reason) {
|
|
|
37
37
|
}
|
|
38
38
|
return reason.slice(0, end) + ELLIPSIS;
|
|
39
39
|
}
|
|
40
|
-
//#endregion
|
|
41
40
|
export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsLogContent.ts
|
|
2
2
|
/**
|
|
3
3
|
* Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
|
|
4
4
|
* le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
|
|
@@ -64,5 +64,4 @@ function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
|
|
|
64
64
|
if (typeof s !== "string") return String(data);
|
|
65
65
|
return s.length > cap ? `${s.slice(0, cap)}…` : s;
|
|
66
66
|
}
|
|
67
|
-
//#endregion
|
|
68
67
|
export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
|
|
@@ -2,7 +2,7 @@ import { extend } from "nodefony";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { parseCookie } from "cookie";
|
|
4
4
|
import MS from "ms";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/cookies/cookie.ts
|
|
6
6
|
const encode = encodeURIComponent;
|
|
7
7
|
const decode = decodeURIComponent;
|
|
8
8
|
const cookieDefaultSettings = {
|
|
@@ -20,11 +20,9 @@ function parser(strToParse) {
|
|
|
20
20
|
}
|
|
21
21
|
function cookiesParser(context) {
|
|
22
22
|
let cookies = null;
|
|
23
|
-
let co
|
|
23
|
+
let co;
|
|
24
24
|
switch (context.type) {
|
|
25
25
|
case "http":
|
|
26
|
-
case "https":
|
|
27
|
-
case "http2":
|
|
28
26
|
if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
|
|
29
27
|
if (cookies) {
|
|
30
28
|
const obj = parser(cookies);
|
|
@@ -35,8 +33,7 @@ function cookiesParser(context) {
|
|
|
35
33
|
if (context.response) context.request.request.cookies = context.response.cookies;
|
|
36
34
|
}
|
|
37
35
|
break;
|
|
38
|
-
case "websocket":
|
|
39
|
-
case "websocket-secure": {
|
|
36
|
+
case "websocket": {
|
|
40
37
|
const cookieHeader = context.request?.headers.cookie;
|
|
41
38
|
if (cookieHeader) {
|
|
42
39
|
const parsed = parser(cookieHeader);
|
|
@@ -254,5 +251,4 @@ var Cookie = class Cookie {
|
|
|
254
251
|
return obj;
|
|
255
252
|
}
|
|
256
253
|
};
|
|
257
|
-
//#endregion
|
|
258
254
|
export { cookiesParser, Cookie as default, parser };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clc, nodefonyError } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/errors/httpError.ts
|
|
3
3
|
const exclude = {
|
|
4
4
|
context: true,
|
|
5
5
|
resolver: true,
|
|
@@ -65,5 +65,4 @@ var HttpError = class extends nodefonyError {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
|
|
68
|
-
//#endregion
|
|
69
68
|
export { HttpError as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/errors/thrownMessage.ts
|
|
2
2
|
/**
|
|
3
3
|
* Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
|
|
4
4
|
*
|
|
@@ -23,5 +23,4 @@ function thrownMessage(value) {
|
|
|
23
23
|
case "function": return value === null ? "null" : Object.prototype.toString.call(value);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
//#endregion
|
|
27
26
|
export { thrownMessage };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/profiler/FrameProfile.ts
|
|
3
3
|
/**
|
|
4
4
|
* Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
|
|
5
5
|
* la connexion qui la porte.
|
|
@@ -37,12 +37,22 @@ var FrameProfile = class {
|
|
|
37
37
|
* invocation, il redevient exact, et le SQL se replace dans le waterfall.
|
|
38
38
|
*/
|
|
39
39
|
profilerQueries;
|
|
40
|
+
/**
|
|
41
|
+
* Ce profil sera-t-il gardé par le Profiler ? Faux quand seul le timing
|
|
42
|
+
* l'a ouvert (chemin d'outillage) : son id ne désigne alors aucun profil
|
|
43
|
+
* lisible, et ne doit pas être annoncé au client.
|
|
44
|
+
*/
|
|
45
|
+
get profiled() {
|
|
46
|
+
return this.profilerQueries !== null;
|
|
47
|
+
}
|
|
40
48
|
/** Resolver de la frame — porte route / controller / action. */
|
|
41
49
|
resolver = null;
|
|
42
50
|
/** Statut HTTP-équivalent de l'invocation (200, 403, 404…). */
|
|
43
51
|
response = null;
|
|
44
52
|
error = null;
|
|
45
53
|
timing;
|
|
54
|
+
clockStartMs;
|
|
55
|
+
durationMs = null;
|
|
46
56
|
constructor(init) {
|
|
47
57
|
this.requestId = init.requestId;
|
|
48
58
|
this.type = init.type;
|
|
@@ -54,9 +64,20 @@ var FrameProfile = class {
|
|
|
54
64
|
this.security = init.security;
|
|
55
65
|
this.securityTrace = init.securityTrace;
|
|
56
66
|
this.timing = init.timing;
|
|
67
|
+
this.clockStartMs = init.timing ? performance.now() : 0;
|
|
57
68
|
this.profilerQueries = init.queries ? [] : null;
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
71
|
+
* Arrête l'horloge de la frame (au premier appel) et rend sa durée en ms ;
|
|
72
|
+
* `null` si le timing est éteint. Contrat de `IContext.stopClock`.
|
|
73
|
+
*
|
|
74
|
+
* @returns la durée de l'invocation, ou `null`
|
|
75
|
+
*/
|
|
76
|
+
stopClock() {
|
|
77
|
+
if (!this.timing) return null;
|
|
78
|
+
return this.durationMs ??= performance.now() - this.clockStartMs;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
60
81
|
* Ouvre une phase. Contrairement au `Context`, l'index est un **scan arrière**
|
|
61
82
|
* (pas de `Map<nom, index>`) : une frame porte une poignée de phases, et une
|
|
62
83
|
* phase peut être ré-entrante (une action qui en déclenche une autre) — la
|
|
@@ -88,9 +109,9 @@ var FrameProfile = class {
|
|
|
88
109
|
* @param error - l'erreur qui a interrompu la frame, le cas échéant.
|
|
89
110
|
*/
|
|
90
111
|
finish(status, error) {
|
|
112
|
+
this.stopClock();
|
|
91
113
|
this.response = { statusCode: status };
|
|
92
114
|
if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
|
|
93
115
|
}
|
|
94
116
|
};
|
|
95
|
-
//#endregion
|
|
96
117
|
export { FrameProfile, FrameProfile as default };
|
|
@@ -1,24 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let end = first.startMs;
|
|
8
|
-
for (const p of phases) {
|
|
9
|
-
const e = p.startMs + (p.durationMs ?? 0);
|
|
10
|
-
if (e > end) end = e;
|
|
11
|
-
}
|
|
12
|
-
const d = end - first.startMs;
|
|
13
|
-
return d >= 0 ? Math.round(d * 1e3) / 1e3 : null;
|
|
1
|
+
import { profileLane } from "./aggregate.js";
|
|
2
|
+
import { frameIndexOf } from "nodefony";
|
|
3
|
+
// nodefony/src/profiler/Profiler.ts
|
|
4
|
+
/** Arrondit une durée à la microseconde (affichage du Suivi). */
|
|
5
|
+
function roundMicro(ms) {
|
|
6
|
+
return ms === null ? null : Math.round(ms * 1e3) / 1e3;
|
|
14
7
|
}
|
|
15
8
|
/** Capacité par défaut du ring buffer (assez pour une session de debug). */
|
|
16
9
|
const DEFAULT_CAP = 500;
|
|
10
|
+
/**
|
|
11
|
+
* Capacité par défaut des deux anneaux annexes : l'OUTILLAGE (console, API
|
|
12
|
+
* d'administration, porte MCP) et les ASSETS relayés de Vite. Un chargement
|
|
13
|
+
* de la console relaie à lui seul quelques centaines de modules : dans un
|
|
14
|
+
* tampon commun, il chassait en quelques secondes les profils de
|
|
15
|
+
* l'application, puis ceux des appels de la console elle-même.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_SIDE_CAP = 100;
|
|
18
|
+
/**
|
|
19
|
+
* Collecteur des profils de requête, en un anneau par provenance
|
|
20
|
+
* ({@link profileLane}) : l'application, l'outillage, les assets relayés.
|
|
21
|
+
* Chacun évince ses propres profils — ouvrir la console ne chasse plus celui
|
|
22
|
+
* qu'on y consulte.
|
|
23
|
+
*/
|
|
17
24
|
var Profiler = class {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
_lanes = {
|
|
26
|
+
app: /* @__PURE__ */ new Map(),
|
|
27
|
+
tooling: /* @__PURE__ */ new Map(),
|
|
28
|
+
asset: /* @__PURE__ */ new Map()
|
|
29
|
+
};
|
|
30
|
+
_caps;
|
|
31
|
+
/** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
|
|
32
|
+
_seq = 0;
|
|
33
|
+
/**
|
|
34
|
+
* @param cap - capacité de l'anneau de l'application.
|
|
35
|
+
* @param sideCap - capacité de chacun des anneaux de l'outillage et des
|
|
36
|
+
* assets relayés.
|
|
37
|
+
*/
|
|
38
|
+
constructor(cap = DEFAULT_CAP, sideCap = DEFAULT_SIDE_CAP) {
|
|
39
|
+
const side = sideCap > 0 ? sideCap : DEFAULT_SIDE_CAP;
|
|
40
|
+
this._caps = {
|
|
41
|
+
app: cap > 0 ? cap : DEFAULT_CAP,
|
|
42
|
+
tooling: side,
|
|
43
|
+
asset: side
|
|
44
|
+
};
|
|
22
45
|
}
|
|
23
46
|
/**
|
|
24
47
|
* Snapshot un `Context` en fin de requête. Tolérant aux champs absents
|
|
@@ -32,13 +55,14 @@ var Profiler = class {
|
|
|
32
55
|
const phases = ctx.phases ?? [];
|
|
33
56
|
const entry = {
|
|
34
57
|
requestId,
|
|
58
|
+
seq: ++this._seq,
|
|
35
59
|
ts: Date.now(),
|
|
36
|
-
kind: ctx.type === "websocket"
|
|
60
|
+
kind: ctx.type === "websocket" ? "ws" : "http",
|
|
37
61
|
method: ctx.method ?? null,
|
|
38
62
|
url: ctx.url ?? "",
|
|
39
63
|
scheme: ctx.scheme ?? "",
|
|
40
64
|
status: ctx.response?.statusCode ?? (ctx.error ? 500 : null),
|
|
41
|
-
durationMs:
|
|
65
|
+
durationMs: roundMicro(ctx.stopClock()),
|
|
42
66
|
route: ctx.resolver?.route?.name ?? null,
|
|
43
67
|
controller: ctx.resolver?.controller?.name ?? null,
|
|
44
68
|
action: ctx.resolver?.actionName ?? null,
|
|
@@ -54,25 +78,36 @@ var Profiler = class {
|
|
|
54
78
|
queries: ctx.profilerQueries && ctx.profilerQueries.length > 0 ? ctx.profilerQueries : void 0,
|
|
55
79
|
security: readSecurity(ctx)
|
|
56
80
|
};
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
const { app, tooling, asset } = this._lanes;
|
|
82
|
+
app.delete(requestId);
|
|
83
|
+
tooling.delete(requestId);
|
|
84
|
+
asset.delete(requestId);
|
|
85
|
+
const lane = profileLane(entry.url);
|
|
86
|
+
const buf = this._lanes[lane];
|
|
87
|
+
buf.set(requestId, entry);
|
|
88
|
+
if (buf.size > this._caps[lane]) {
|
|
89
|
+
const oldest = buf.keys().next().value;
|
|
90
|
+
if (oldest !== void 0) buf.delete(oldest);
|
|
62
91
|
}
|
|
63
92
|
}
|
|
64
93
|
/** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
|
|
65
94
|
get(requestId) {
|
|
66
|
-
|
|
95
|
+
const { app, tooling, asset } = this._lanes;
|
|
96
|
+
return app.get(requestId) ?? tooling.get(requestId) ?? asset.get(requestId);
|
|
67
97
|
}
|
|
68
98
|
/**
|
|
69
99
|
* Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
|
|
100
|
+
*
|
|
101
|
+
* @param limit - nombre maximal de résumés
|
|
102
|
+
* @param connection - le requestId d'une connexion WebSocket : ses SEULES
|
|
103
|
+
* frames du pont (`<connection>.<n>`, règle `frameIndexOf` du cœur)
|
|
70
104
|
*/
|
|
71
|
-
recent(limit = 60) {
|
|
105
|
+
recent(limit = 60, connection) {
|
|
72
106
|
const out = [];
|
|
73
|
-
const entries =
|
|
74
|
-
for (const e of entries
|
|
107
|
+
const entries = this.#collectAfter(0).reverse();
|
|
108
|
+
for (const e of entries) {
|
|
75
109
|
if (out.length >= limit) break;
|
|
110
|
+
if (connection && frameIndexOf(e.requestId, connection) === null) continue;
|
|
76
111
|
out.push({
|
|
77
112
|
requestId: e.requestId,
|
|
78
113
|
ts: e.ts,
|
|
@@ -87,13 +122,51 @@ var Profiler = class {
|
|
|
87
122
|
}
|
|
88
123
|
return out;
|
|
89
124
|
}
|
|
125
|
+
/** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
|
|
126
|
+
get cursor() {
|
|
127
|
+
return this._seq;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Profils collectés après `cursor`, tous anneaux confondus, dans l'ordre
|
|
131
|
+
* des numéros.
|
|
132
|
+
*
|
|
133
|
+
* Les pertes se comptent sans estimation : chaque numéro attribué est un
|
|
134
|
+
* profil collecté, donc ce qui manque entre le curseur et le dernier numéro
|
|
135
|
+
* a disparu avant d'être lu. Un curseur venu d'ailleurs (supérieur au
|
|
136
|
+
* dernier numéro) repart de zéro.
|
|
137
|
+
*
|
|
138
|
+
* @param cursor - le `cursor` rendu par l'appel précédent, ou {@link cursor}.
|
|
139
|
+
*/
|
|
140
|
+
since(cursor) {
|
|
141
|
+
const from = cursor >= 0 && cursor <= this._seq ? cursor : 0;
|
|
142
|
+
const entries = this.#collectAfter(from);
|
|
143
|
+
return {
|
|
144
|
+
entries,
|
|
145
|
+
cursor: this._seq,
|
|
146
|
+
lost: this._seq - from - entries.length
|
|
147
|
+
};
|
|
148
|
+
}
|
|
90
149
|
/** Nombre de profils en mémoire. */
|
|
91
150
|
get size() {
|
|
92
|
-
|
|
151
|
+
const { app, tooling, asset } = this._lanes;
|
|
152
|
+
return app.size + tooling.size + asset.size;
|
|
93
153
|
}
|
|
94
|
-
/** Vide
|
|
154
|
+
/** Vide les anneaux. */
|
|
95
155
|
clear() {
|
|
96
|
-
this.
|
|
156
|
+
const { app, tooling, asset } = this._lanes;
|
|
157
|
+
app.clear();
|
|
158
|
+
tooling.clear();
|
|
159
|
+
asset.clear();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
|
|
163
|
+
* ancien au plus récent. Lecture à la demande (console, outil de l'agent) :
|
|
164
|
+
* jamais sur le chemin d'une requête.
|
|
165
|
+
*/
|
|
166
|
+
#collectAfter(from) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const lane of Object.values(this._lanes)) for (const e of lane.values()) if (e.seq > from) out.push(e);
|
|
169
|
+
return out.sort((a, b) => a.seq - b.seq);
|
|
97
170
|
}
|
|
98
171
|
};
|
|
99
172
|
/**
|
|
@@ -131,5 +204,4 @@ function readSecurity(ctx) {
|
|
|
131
204
|
function readUser(ctx) {
|
|
132
205
|
return ctx.userIdentifier ?? ctx.securityTrace?.user ?? null;
|
|
133
206
|
}
|
|
134
|
-
//#endregion
|
|
135
207
|
export { Profiler, Profiler as default };
|