@nodefony/framework 10.0.0-alpha.8 → 10.0.0-beta.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 +4 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +7 -6
- package/dist/nodefony/controller/AdminApiController.js +4 -3
- package/dist/nodefony/controller/ApiKeyController.js +1 -1
- package/dist/nodefony/controller/OAuth2Controller.js +11 -6
- package/dist/nodefony/controller/SessionAuthController.js +3 -3
- package/dist/nodefony/controller/TokenAuthController.js +2 -2
- package/dist/nodefony/controller/TotpController.js +5 -4
- package/dist/nodefony/controller/WebAuthnController.js +4 -4
- package/dist/nodefony/decorators/routerDecorators.js +41 -35
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -5
- package/dist/nodefony/service/router.js +75 -25
- package/dist/nodefony/src/Controller.js +184 -78
- package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
- package/dist/nodefony/src/KernelAdminApi.js +60 -55
- package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
- package/dist/nodefony/src/Resolver.js +219 -63
- package/dist/nodefony/src/Route.js +38 -38
- package/dist/nodefony/src/SyslogAdminApi.js +4 -4
- package/dist/nodefony/src/docsReader.js +22 -18
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/singletonGuard.js +76 -0
- package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
- package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
- package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
- package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
- package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
- package/dist/types/nodefony/service/router.d.ts +33 -7
- package/dist/types/nodefony/src/Controller.d.ts +97 -24
- package/dist/types/nodefony/src/Resolver.d.ts +154 -15
- package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
- package/dist/types/nodefony/src/Route.d.ts +24 -22
- package/dist/types/nodefony/src/docsReader.d.ts +3 -3
- package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
- package/docs/admin.md +26 -26
- package/docs/controller.md +139 -121
- package/docs/decorateurs.md +108 -105
- package/docs/idempotence.md +78 -71
- package/docs/routing.md +57 -51
- package/docs/templates.md +17 -17
- package/package.json +12 -12
package/docs/controller.md
CHANGED
|
@@ -17,24 +17,25 @@ coverageFiles: Controller.ts,Resolver.ts
|
|
|
17
17
|
# Controller — le code de ta route
|
|
18
18
|
|
|
19
19
|
> Une fois la route trouvée, quelqu'un doit **faire le travail** : c'est le contrôleur. Nodefony
|
|
20
|
-
> l'instancie
|
|
20
|
+
> l'instancie (DI compris) — une fois par défaut, à chaque requête sous `@Scope("request")` —, appelle ton action, puis traduit ce que tu **retournes** en
|
|
21
21
|
> réponse HTTP ou en frame WebSocket. Cette page décrit ce qui se passe **dans** le contrôleur : de
|
|
22
22
|
> quoi il hérite, son cycle de vie réel (dont `initialize()`), d'où viennent `request`/`response`/
|
|
23
23
|
> `session`, comment répondre, comment échouer proprement. Tout est ancré sur le code.
|
|
24
24
|
|
|
25
25
|
📍 [Documentation](../../../../../docs/index.md) › [Framework](index.md) › **Controller**
|
|
26
26
|
|
|
27
|
-
## 🧠 Le modèle mental —
|
|
27
|
+
## 🧠 Le modèle mental — du code partagé entre deux mondes
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Par défaut, un contrôleur est un **singleton** : construit à sa première requête, puis partagé par
|
|
30
|
+
toutes les suivantes, concurrentes comprises. Il ne porte que du **code** ; l'état d'une requête
|
|
31
|
+
lui arrive par les arguments décorés et les helpers, qui retrouvent la requête EN COURS. Il vit entre deux mondes qu'il ne connaît pas :
|
|
31
32
|
le **transport** (le contexte HTTP ou WebSocket, fourni par `@nodefony/http`) et le **container**
|
|
32
33
|
(tes services, fournis par le DI).
|
|
33
34
|
|
|
34
35
|
```mermaid
|
|
35
36
|
flowchart LR
|
|
36
37
|
RT["Router<br/>route trouvée"] --> RS["Resolver<br/>par requête"]
|
|
37
|
-
RS -->|"instantiate + DI"| CT["TON Controller<br/>extends Controller"]
|
|
38
|
+
RS -->|"instantiate + DI<br/>(une fois si singleton)"| CT["TON Controller<br/>extends Controller"]
|
|
38
39
|
CT -->|"initialize()"| CT
|
|
39
40
|
RS -->|"action(...args)"| CT
|
|
40
41
|
CT -->|"return valeur"| RC["returnController<br/>traduit le retour"]
|
|
@@ -45,11 +46,11 @@ flowchart LR
|
|
|
45
46
|
|
|
46
47
|
Trois idées à retenir :
|
|
47
48
|
|
|
48
|
-
1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:
|
|
49
|
+
1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:146`). Tu récupères
|
|
49
50
|
donc gratuitement le container (`this.get()`), les logs (`this.log()`) et les événements.
|
|
50
51
|
2. **Tu ne construis rien toi-même** — le `Resolver` instancie ta classe via l'injecteur
|
|
51
|
-
(`Resolver.newController()`, `Resolver.ts:
|
|
52
|
-
3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:
|
|
52
|
+
(`Resolver.newController()`, `Resolver.ts:282`), jamais un `new` direct.
|
|
53
|
+
3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:857`) traduit la
|
|
53
54
|
valeur retournée : objet → JSON, string → corps brut, `void` → « j'ai répondu moi-même ».
|
|
54
55
|
|
|
55
56
|
## 📖 Lexique
|
|
@@ -65,7 +66,7 @@ Trois idées à retenir :
|
|
|
65
66
|
| Auto-JSON | Le fait qu'un objet retourné par l'action devienne une réponse `application/json` sans le demander. |
|
|
66
67
|
| Handshake | La poignée de main d'ouverture d'une connexion WebSocket (avant tout message). |
|
|
67
68
|
| Frame | Un message WebSocket individuel, une fois la connexion ouverte. |
|
|
68
|
-
| Scope (contrôleur) | `"
|
|
69
|
+
| Scope (contrôleur) | `"singleton"` (une instance partagée, défaut) ou `"request"` (une instance par requête). |
|
|
69
70
|
| `waitAsync` | Drapeau posé quand le framework conclut « l'action enverra la réponse elle-même, plus tard ». |
|
|
70
71
|
|
|
71
72
|
## 🚀 Démarrage rapide
|
|
@@ -95,7 +96,8 @@ interface CatalogService {
|
|
|
95
96
|
|
|
96
97
|
@controller("/api/catalog")
|
|
97
98
|
class CatalogController extends Controller {
|
|
98
|
-
//
|
|
99
|
+
// Posé UNE fois dans `initialize()` : le contrôleur est un singleton par défaut,
|
|
100
|
+
// partagé par toutes les requêtes — ce champ ne doit plus changer ensuite.
|
|
99
101
|
private catalog: CatalogService | null = null;
|
|
100
102
|
|
|
101
103
|
// Le contexte de la requête est le SEUL argument obligatoire ; le nom passé à
|
|
@@ -104,8 +106,9 @@ class CatalogController extends Controller {
|
|
|
104
106
|
super("catalog", context);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
// Hook optionnel, appelé à
|
|
108
|
-
// Voir « Le cycle de vie » :
|
|
109
|
+
// Hook optionnel, appelé UNE fois, à la création du singleton (à chaque requête
|
|
110
|
+
// sous `@Scope("request")`). Voir « Le cycle de vie » : ni session ni
|
|
111
|
+
// utilisateur n'y sont résolus.
|
|
109
112
|
async initialize(): Promise<this> {
|
|
110
113
|
this.catalog = this.get<CatalogService>("catalog");
|
|
111
114
|
return this;
|
|
@@ -195,15 +198,15 @@ Le tableau ci-dessous donne la séquence exacte, avec l'ancre qui la prouve :
|
|
|
195
198
|
|
|
196
199
|
| # | Étape | Où |
|
|
197
200
|
| --- | --------------------------------------- | --------------------------------------------------- |
|
|
198
|
-
| 1 | Appariement de la route | `router.resolve()` (`http-kernel.ts:
|
|
199
|
-
| 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:
|
|
200
|
-
| 3 | Parse du corps (sauf `@Body({stream})`) | `http-kernel.ts:
|
|
201
|
-
| 4 | Armement de la route (sans instance) | `prepareFrontController()` (`http-kernel.ts:
|
|
202
|
-
| 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:
|
|
203
|
-
| 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:
|
|
204
|
-
| 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:
|
|
205
|
-
| 8 | Autorisation `@IsGranted` | `Resolver.executeAction()` (`Resolver.ts:
|
|
206
|
-
| 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:
|
|
201
|
+
| 1 | Appariement de la route | `router.resolve()` (`http-kernel.ts:807`) |
|
|
202
|
+
| 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1418`) |
|
|
203
|
+
| 3 | Parse du corps (sauf `@Body({stream})`) | `http-kernel.ts:1449` |
|
|
204
|
+
| 4 | Armement de la route (sans instance) | `prepareFrontController()` (`http-kernel.ts:789`) |
|
|
205
|
+
| 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1524`) |
|
|
206
|
+
| 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1168`) |
|
|
207
|
+
| 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1535`) |
|
|
208
|
+
| 8 | Autorisation `@IsGranted` | `Resolver.executeAction()` (`Resolver.ts:372`) |
|
|
209
|
+
| 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:372`) |
|
|
207
210
|
| 10 | **Ton action** | `controller[methodKey]()` (`Resolver.ts:382`) |
|
|
208
211
|
|
|
209
212
|
> [!IMPORTANT]
|
|
@@ -219,25 +222,29 @@ Le tableau ci-dessous donne la séquence exacte, avec l'ancre qui la prouve :
|
|
|
219
222
|
C'est **sa raison d'être** : un `constructor` ne peut pas être `async`, et la résolution DI est
|
|
220
223
|
synchrone. Tout ce qui demande un `await` à la mise en place de l'instance n'a pas d'autre endroit
|
|
221
224
|
où aller. Le hook est **optionnel** — le Resolver ne l'appelle que s'il existe
|
|
222
|
-
(`Resolver._createController()`, `Resolver.ts:
|
|
225
|
+
(`Resolver._createController()`, `Resolver.ts:330`). Son contrat est décrit par
|
|
223
226
|
`ControllerWithInitialize` (`Resolver.ts:72`) : aucun argument, retour `Promise<this>`.
|
|
224
227
|
|
|
228
|
+
> [!IMPORTANT]
|
|
229
|
+
> **Sur un contrôleur singleton — le défaut —, `initialize()` tourne UNE fois**, à la création, dans
|
|
230
|
+
> le contexte de la requête qui l'a déclenchée ; l'instance sert ensuite toutes les autres. Tout ce
|
|
231
|
+
> qui dépend de LA requête (identité, cookie, mode de rendu) y serait figé pour tout le monde : sa
|
|
232
|
+
> place est dans l'action. Sous `@Scope("request")`, le hook retrouve sa sémantique par requête.
|
|
233
|
+
|
|
225
234
|
```typescript
|
|
226
235
|
async initialize(): Promise<this> {
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
this.prefs = await this.get<Prefs>("prefs").load(user.identifier);
|
|
236
|
+
this.catalog = this.get<CatalogService>("catalog"); // service résolu une fois
|
|
237
|
+
this.defaults = await this.get<Prefs>("prefs").loadDefaults(); // préchauffage partagé
|
|
230
238
|
return this; // toujours rendre `this`
|
|
231
239
|
}
|
|
232
240
|
```
|
|
233
241
|
|
|
234
|
-
| Dans `initialize()
|
|
235
|
-
|
|
|
236
|
-
| Un `await` de mise en place :
|
|
237
|
-
| Résoudre des services (`this.get("catalog")`)
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
| Choisir un mode de rendu (`this.setContextHtml()`) | Lire `this.session` sans l'avoir demandée : elle reste **lazy** (`@UseSession`, cf plus bas) |
|
|
242
|
+
| Dans `initialize()` d'un singleton, tu peux… | Ce qui n'a rien à y faire |
|
|
243
|
+
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
244
|
+
| Un `await` de mise en place : ouvrir une ressource, précalculer, préchauffer | Une **décision d'autorisation** — c'est `@IsGranted`, évalué avant, et un 403 n'arrive jamais jusqu'ici |
|
|
245
|
+
| Résoudre des services (`this.get("catalog")`) | Lire l'identité, poser un cookie, choisir un mode de rendu : c'est l'état d'UNE requête → l'action, ou `@Scope("request")` |
|
|
246
|
+
| Poser des champs de démarrage sur `this` — le filet de développement ne les garde qu'APRÈS le hook | Écrire sur `this` ensuite : en développement, l'écriture lève (l'état fuirait vers la requête suivante) |
|
|
247
|
+
| — | Une écriture longue qui bloque : la phase `initialize` est chronométrée, elle apparaîtra dans la debug bar |
|
|
241
248
|
|
|
242
249
|
> [!NOTE]
|
|
243
250
|
> **La session ne s'ouvre pas ici.** Nodefony a un point d'activation **unique**
|
|
@@ -250,14 +257,17 @@ async initialize(): Promise<this> {
|
|
|
250
257
|
Si ton `initialize()` lève, l'exception remonte le pipeline et sort en réponse d'erreur cohérente :
|
|
251
258
|
**500 JSON**, serveur toujours sain. C'est prouvé par une sonde dédiée du dépôt
|
|
252
259
|
(`LifecycleController.initialize()`, `LifecycleController.ts:21`, exercée par
|
|
253
|
-
`lifecycle-init-crash.test.ts`). Aucune requête pendue, aucun timeout muet.
|
|
260
|
+
`lifecycle-init-crash.test.ts`). Aucune requête pendue, aucun timeout muet. Pour un singleton,
|
|
261
|
+
une création en échec ne reste pas en cache : la requête suivante recrée l'instance
|
|
262
|
+
(`Router.getSingletonController()`, `router.ts:197`) — une base pas encore prête au premier appel ne
|
|
263
|
+
tue pas le contrôleur jusqu'au redémarrage.
|
|
254
264
|
|
|
255
265
|
### Les phases mesurées
|
|
256
266
|
|
|
257
267
|
Chaque étape est chronométrée sous le nom d'une **phase**, lisible dans la debug bar et le profileur :
|
|
258
268
|
`resolve` · `initialize` (DI + ton hook) · `parse` · `firewall` · `action` · `render` · `send`.
|
|
259
269
|
La phase `initialize` existe précisément pour que le temps passé dans ton hook et dans la résolution
|
|
260
|
-
DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:
|
|
270
|
+
DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:112`).
|
|
261
271
|
|
|
262
272
|
## 🔌 HTTP et WebSocket — le même contrôleur
|
|
263
273
|
|
|
@@ -292,27 +302,28 @@ class ChatController extends Controller {
|
|
|
292
302
|
| Aspect | HTTP | WebSocket |
|
|
293
303
|
| --- | --- | --- |
|
|
294
304
|
| Durée de vie du contexte | Une requête | **Toute la connexion** |
|
|
295
|
-
| Instance du contrôleur |
|
|
296
|
-
| Nombre d'appels d'action | 1 | 1 au handshake (`WebsocketContext.handle()`, `WebsocketContext.ts:
|
|
305
|
+
| Instance du contrôleur | Singleton (défaut) : **la même** pour toutes les requêtes · `@Scope("request")` : une par requête | Singleton : **la même** pour toutes les connexions · `@Scope("request")` : **une par connexion**, réutilisée à chaque frame |
|
|
306
|
+
| Nombre d'appels d'action | 1 | 1 au handshake (`WebsocketContext.handle()`, `WebsocketContext.ts:271`) + 1 par frame (`handleMessage()`, `WebsocketContext.ts:479`) |
|
|
297
307
|
| Argument de l'action | Variables de route (ou paramètres décorés) | Idem + **le message** en dernier argument (`WebsocketContext.ts:508`) |
|
|
298
308
|
| Rendu d'un `return` | Corps de la réponse | Frame envoyée sur la socket |
|
|
299
309
|
| Échec | Statut HTTP + corps d'erreur | **Code de fermeture** RFC 6455 (401/403 → 1008, 5xx → 1011, autre → 4004) |
|
|
300
|
-
| `initialize()` |
|
|
310
|
+
| `initialize()` | Singleton : **une fois**, à la création · `@Scope("request")` : à chaque requête | Singleton : une fois, à la création · `@Scope("request")` : **une fois par connexion**, au handshake |
|
|
301
311
|
|
|
302
312
|
La réutilisation de l'instance vient du cache posé sur le container du contexte
|
|
303
|
-
(`Resolver.newController()`, `Resolver.ts:
|
|
313
|
+
(`Resolver.newController()`, `Resolver.ts:282`) : le contexte WS étant partagé par la connexion, le
|
|
304
314
|
contrôleur l'est aussi. Un garde-fou vérifie que l'instance cachée est bien de la classe de la route
|
|
305
315
|
courante et la reconstruit sinon (`Resolver.ts:344-347`) — sans quoi un message invoquant une autre
|
|
306
316
|
action se tromperait d'objet.
|
|
307
317
|
|
|
308
318
|
> [!WARNING]
|
|
309
319
|
> **Sur une connexion WS, `this` survit aux frames.** Un champ écrit à la frame 1 est encore là à la
|
|
310
|
-
> frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve.
|
|
311
|
-
>
|
|
320
|
+
> frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve. Sous
|
|
321
|
+
> `@Scope("request")`, chaque requête HTTP repart d'une instance neuve ; un singleton, lui, refuse
|
|
322
|
+
> tout état de requête sur `this`.
|
|
312
323
|
|
|
313
|
-
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:
|
|
324
|
+
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1779`)
|
|
314
325
|
appelle `handleFrontController()` (donc `initialize()`) **avant** `startSession()`
|
|
315
|
-
(`http-kernel.ts:
|
|
326
|
+
(`http-kernel.ts:1168`), avant l'acceptation de la socket, et avant le firewall
|
|
316
327
|
(`http-kernel.ts:1457`).
|
|
317
328
|
|
|
318
329
|
## 🧠 D'où viennent `request`, `response`, `session`
|
|
@@ -322,16 +333,16 @@ sont des accesseurs qui dérivent du contexte **vivant**, selon le motif `champ
|
|
|
322
333
|
|
|
323
334
|
| Raccourci | Ce que tu obtiens | Ancre |
|
|
324
335
|
| ---------------- | -------------------------------------------------- | ------------------- |
|
|
325
|
-
| `this.context` | Le contexte transport de la requête courante | `Controller.ts:
|
|
326
|
-
| `this.route` | La route matchée | `Controller.ts:
|
|
327
|
-
| `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:
|
|
328
|
-
| `this.response` | La réponse du transport | `Controller.ts:
|
|
336
|
+
| `this.context` | Le contexte transport de la requête courante | `Controller.ts:238` |
|
|
337
|
+
| `this.route` | La route matchée | `Controller.ts:202` |
|
|
338
|
+
| `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:269` |
|
|
339
|
+
| `this.response` | La réponse du transport | `Controller.ts:277` |
|
|
329
340
|
| `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:212` |
|
|
330
|
-
| `this.queryGet` | Les paramètres de la query string | `Controller.ts:
|
|
331
|
-
| `this.queryPost` | Le corps parsé | `Controller.ts:
|
|
332
|
-
| `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:
|
|
333
|
-
| `this.queryFile` | Les fichiers uploadés | `Controller.ts:
|
|
334
|
-
| `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:
|
|
341
|
+
| `this.queryGet` | Les paramètres de la query string | `Controller.ts:302` |
|
|
342
|
+
| `this.queryPost` | Le corps parsé | `Controller.ts:351` |
|
|
343
|
+
| `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:351` |
|
|
344
|
+
| `this.queryFile` | Les fichiers uploadés | `Controller.ts:341` |
|
|
345
|
+
| `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:384` |
|
|
335
346
|
|
|
336
347
|
Pourquoi des accesseurs plutôt que des champs recopiés au constructeur : **la fraîcheur et le coût**.
|
|
337
348
|
Une valeur recopiée vieillit dès que le pipeline modifie le contexte, et recopier quatre structures
|
|
@@ -340,7 +351,7 @@ gratuitement.
|
|
|
340
351
|
|
|
341
352
|
### La session est **lazy** — elle n'existe que si tu la demandes
|
|
342
353
|
|
|
343
|
-
`this.session` est un simple getter sur `context.session` (`Controller.ts:
|
|
354
|
+
`this.session` est un simple getter sur `context.session` (`Controller.ts:384`). Il n'y a **pas** de
|
|
344
355
|
`startSession()` à appeler depuis un contrôleur : l'activation se déclare sur la route, avec
|
|
345
356
|
`@UseSession()` (ou un paramètre `@Session()`, qui vaut déclaration implicite), et le pipeline
|
|
346
357
|
l'exécute à son point unique. Sans déclaration et sans cookie de session entrant, **aucune session
|
|
@@ -349,64 +360,70 @@ n'est créée** — donc aucun coût de stockage.
|
|
|
349
360
|
Deux corollaires :
|
|
350
361
|
|
|
351
362
|
- Dans `initialize()`, `this.session` vaut `null` (l'activation vient plus tard — étape 6 du cycle).
|
|
352
|
-
- `this.getSession()` (`Controller.ts:
|
|
363
|
+
- `this.getSession()` (`Controller.ts:630`) ne « démarre » rien : il retourne la session existante,
|
|
353
364
|
ou `undefined`.
|
|
354
365
|
|
|
355
|
-
Les messages flash s'appuient dessus : `setFlashBag()`/`addFlash()` (`Controller.ts:
|
|
356
|
-
`getFlashBag()` (`Controller.ts:
|
|
366
|
+
Les messages flash s'appuient dessus : `setFlashBag()`/`addFlash()` (`Controller.ts:666`) et
|
|
367
|
+
`getFlashBag()` (`Controller.ts:650`) journalisent une **erreur** et retournent `null` si aucune
|
|
357
368
|
session n'est active — pas de crash, mais rien n'est mémorisé.
|
|
358
369
|
|
|
359
|
-
### Contrôleur
|
|
370
|
+
### Contrôleur singleton — le défaut : `this` n'est pas à toi
|
|
371
|
+
|
|
372
|
+
Par défaut, `Controller.scope` vaut `"singleton"` (`Controller.ts:182`) : UNE instance par classe,
|
|
373
|
+
construite à sa première requête puis partagée par toutes les suivantes, concurrentes comprises.
|
|
360
374
|
|
|
361
|
-
|
|
362
|
-
|
|
375
|
+
- **une seule instance** pour tout le process, liée au container du **kernel**, pas à celui de la
|
|
376
|
+
requête (`Controller.ts:399-411`) — capturer le container de requête serait fatal, il est nettoyé
|
|
377
|
+
à la fin de la requête ;
|
|
378
|
+
- **`initialize()` n'est appelé qu'une fois**, à la création (sémantique « démarrage ») ;
|
|
379
|
+
- le contexte n'est pas posé sur l'objet : `this.context` **lit l'ALS**
|
|
380
|
+
(`RequestContext.getContext()`, `Controller.ts:223`) et retrouve donc la requête réellement en
|
|
381
|
+
cours, jamais celle d'une requête concurrente — y compris la route et la query d'un appel du pont
|
|
382
|
+
`api.request` (`Controller.ts:269`).
|
|
383
|
+
|
|
384
|
+
Deux filets rendent visible l'état de requête posé sur `this` : les accesseurs (`this.query = …`,
|
|
385
|
+
`setRoute()`…) **lèvent** dans tous les environnements, et **en développement** toute écriture sur
|
|
386
|
+
un champ lève aussi, en nommant le contrôleur, le champ et le remède.
|
|
387
|
+
|
|
388
|
+
> [!WARNING]
|
|
389
|
+
> Ce que les filets ne voient pas — un champ `#privé`, un objet muté (`this.cache.set(…)`) — reste
|
|
390
|
+
> une **fuite de données entre utilisateurs** silencieuse. Et un callback qui s'exécute HORS de la
|
|
391
|
+
> bulle de la requête (écouteur d'un émetteur, minuteur armé ailleurs) n'y retrouve pas
|
|
392
|
+
> `this.context` : lire le contexte AVANT, dans une variable.
|
|
393
|
+
|
|
394
|
+
Besoin d'une instance **par requête** — injecter un service de portée `request`, préparer chaque
|
|
395
|
+
requête dans `initialize()`, garder un état sur `this` ? Déclarer la classe `@Scope("request")` :
|
|
363
396
|
|
|
364
397
|
```typescript
|
|
365
|
-
@Scope("
|
|
366
|
-
@controller("/api/
|
|
367
|
-
class
|
|
398
|
+
@Scope("request")
|
|
399
|
+
@controller("/api/me")
|
|
400
|
+
class MeController extends Controller {
|
|
368
401
|
/* … */
|
|
369
402
|
}
|
|
370
403
|
```
|
|
371
404
|
|
|
372
|
-
Ce que ça change, concrètement :
|
|
373
|
-
|
|
374
|
-
- **une seule instance** pour tout le process, bindée au container du **kernel**, pas à celui de la
|
|
375
|
-
requête (`Controller.ts:244-252`) — capturer le container de requête serait fatal, il est nettoyé
|
|
376
|
-
au teardown ;
|
|
377
|
-
- **`initialize()` n'est appelé qu'une fois**, à la création (sémantique « boot ») ;
|
|
378
|
-
- le contexte n'est plus posé sur l'objet : `this.context` **retombe sur l'ALS**
|
|
379
|
-
(`RequestContext.getContext()`, `Controller.ts:147`) et retrouve donc la requête réellement en
|
|
380
|
-
cours, jamais celle d'une requête concurrente.
|
|
381
|
-
|
|
382
|
-
> [!WARNING]
|
|
383
|
-
> Un champ mutable par requête sur un contrôleur `singleton` est une **fuite de données entre
|
|
384
|
-
> utilisateurs** silencieuse (requête A lit ce qu'a écrit requête B). N'utilise `@Scope("singleton")`
|
|
385
|
-
> que si toutes tes données passent par les arguments décorés et l'ALS. Le défaut per-requête reste
|
|
386
|
-
> le choix sûr — le gain mesuré du singleton est dans le bruit de mesure.
|
|
387
|
-
|
|
388
405
|
## 🧰 Répondre — ce que ton `return` déclenche
|
|
389
406
|
|
|
390
|
-
Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:
|
|
407
|
+
Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:857`). Il regarde le **type**
|
|
391
408
|
de ce que tu as retourné :
|
|
392
409
|
|
|
393
410
|
<!-- prettier-ignore -->
|
|
394
411
|
| Tu retournes… | Ce qui se passe | Ancre |
|
|
395
412
|
| --- | --- | --- |
|
|
396
|
-
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:
|
|
413
|
+
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:830-840` |
|
|
397
414
|
| Une `string` | Envoyée telle quelle en corps | `Resolver.ts:711` |
|
|
398
|
-
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:
|
|
415
|
+
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:936` |
|
|
399
416
|
| Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts:734` |
|
|
400
|
-
| Un `Buffer` | Envoyé brut | `Resolver.ts:
|
|
401
|
-
| Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:
|
|
402
|
-
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:
|
|
403
|
-
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:
|
|
404
|
-
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:
|
|
417
|
+
| Un `Buffer` | Envoyé brut | `Resolver.ts:900` |
|
|
418
|
+
| Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:581` |
|
|
419
|
+
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:1002`) |
|
|
420
|
+
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:981` |
|
|
421
|
+
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:942-951` |
|
|
405
422
|
|
|
406
423
|
> [!WARNING]
|
|
407
424
|
> **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
|
|
408
425
|
> répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
|
|
409
|
-
> `NO_BODY_STATUS` (`Resolver.ts:
|
|
426
|
+
> `NO_BODY_STATUS` (`Resolver.ts:1002`) contient 204, 205 et 304. Donc un `@Delete` qui fait
|
|
410
427
|
> `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
|
|
411
428
|
> laisse la requête pendue.
|
|
412
429
|
|
|
@@ -419,25 +436,25 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
|
|
|
419
436
|
|
|
420
437
|
| Helper | Pour… | Ancre |
|
|
421
438
|
| -------------------------------------------- | -------------------------------------------------------- | ------------------- |
|
|
422
|
-
| `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:
|
|
423
|
-
| `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:
|
|
424
|
-
| `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:
|
|
425
|
-
| `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:
|
|
426
|
-
| `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:
|
|
427
|
-
| `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:
|
|
428
|
-
| `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:
|
|
439
|
+
| `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:602` |
|
|
440
|
+
| `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:485` |
|
|
441
|
+
| `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:524` |
|
|
442
|
+
| `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:503` |
|
|
443
|
+
| `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:636` |
|
|
444
|
+
| `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:670` |
|
|
445
|
+
| `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:430` |
|
|
429
446
|
|
|
430
447
|
`renderView()` mesure sa propre phase `render` et injecte automatiquement les aides frontend
|
|
431
448
|
(`frontendTags`, `frontendDocument`, `asset`) dans les variables du template
|
|
432
|
-
(`withFrontendLocals()`, `Controller.ts:
|
|
449
|
+
(`withFrontendLocals()`, `Controller.ts:562`) — tes propres valeurs restent prioritaires.
|
|
433
450
|
|
|
434
451
|
`forward()` re-résout un contrôleur sur le **même** contexte et rappelle son action
|
|
435
|
-
(`Controller.ts:
|
|
452
|
+
(`Controller.ts:670`) : c'est une délégation interne, la requête cliente reste unique.
|
|
436
453
|
|
|
437
454
|
> [!TIP]
|
|
438
455
|
> **Redirection : le code par défaut est 302** (Found), pas 301. Un statut absent ou hors de la liste
|
|
439
456
|
> RFC 9110 §15.4 (301, 302, 303, 307, 308) retombe sur 302 avec un log d'avertissement
|
|
440
|
-
> (`Response.redirect()`, `Response.ts:
|
|
457
|
+
> (`Response.redirect()`, `Response.ts:666`). Un 301 par défaut piégeait : les navigateurs le mettent
|
|
441
458
|
> en cache de façon quasi irréversible.
|
|
442
459
|
|
|
443
460
|
## 📁 Servir un fichier — téléchargement et flux média
|
|
@@ -446,15 +463,15 @@ Deux besoins distincts, deux helpers.
|
|
|
446
463
|
|
|
447
464
|
### Téléchargement — `renderFileDownload()`
|
|
448
465
|
|
|
449
|
-
`renderFileDownload(file, options?, headers?)` (`Controller.ts:
|
|
466
|
+
`renderFileDownload(file, options?, headers?)` (`Controller.ts:761`) pose
|
|
450
467
|
`Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
|
|
451
|
-
de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:
|
|
468
|
+
de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:722`) ;
|
|
452
469
|
la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
|
|
453
470
|
et gèle le process le temps du stat.
|
|
454
471
|
|
|
455
472
|
### Lecture en continu — `renderMediaStream()`
|
|
456
473
|
|
|
457
|
-
`renderMediaStream(file, headers?, options?)` (`Controller.ts:
|
|
474
|
+
`renderMediaStream(file, headers?, options?)` (`Controller.ts:928`) implémente les **requêtes par
|
|
458
475
|
plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
|
|
459
476
|
|
|
460
477
|
| Le client envoie… | Réponse |
|
|
@@ -470,11 +487,11 @@ donc testable sans serveur.
|
|
|
470
487
|
|
|
471
488
|
### Ce que `streamFile()` garantit
|
|
472
489
|
|
|
473
|
-
`streamFile()` (`Controller.ts:
|
|
490
|
+
`streamFile()` (`Controller.ts:801`) est le moteur commun. Sa subtilité n'est pas le pipe, c'est le
|
|
474
491
|
**nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
|
|
475
492
|
téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
|
|
476
493
|
écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
|
|
477
|
-
résout la promesse (`Controller.ts:
|
|
494
|
+
résout la promesse (`Controller.ts:762-804`), puis se retire lui-même (`Controller.ts:746`). Un
|
|
478
495
|
téléchargement interrompu ne coûte donc **rien** en ressource retenue.
|
|
479
496
|
|
|
480
497
|
## ⚠️ Erreurs — lever, rendre, observer
|
|
@@ -489,11 +506,11 @@ throw new nodefonyError("Article introuvable", 404); // statut porté par l'erre
|
|
|
489
506
|
throw new HttpError("Not Found", 404, this.context); // variante enrichie du contexte
|
|
490
507
|
```
|
|
491
508
|
|
|
492
|
-
L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:
|
|
509
|
+
L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:896`), qui délègue la mise en
|
|
493
510
|
forme au rendeur d'erreurs. Ce qui en sort :
|
|
494
511
|
|
|
495
512
|
- **statut normalisé** — un code absent (ou l'ancien quirk `200`) devient **500**
|
|
496
|
-
(`normalizeHttpStatus()`, `error-renderer.ts:
|
|
513
|
+
(`normalizeHttpStatus()`, `error-renderer.ts:642`) ;
|
|
497
514
|
- **corps structuré** : `{ code, message, result: null, error: {…}, nodefony: {…} }`, l'enveloppe
|
|
498
515
|
`nodefony` portant l'environnement, l'URL et l'**identifiant de requête** — de quoi retrouver la
|
|
499
516
|
trace complète dans les logs ;
|
|
@@ -501,13 +518,13 @@ forme au rendeur d'erreurs. Ce qui en sort :
|
|
|
501
518
|
ne tente pas de rendre — il journalise et s'arrête (`http-kernel.ts:770-775`).
|
|
502
519
|
|
|
503
520
|
En **WebSocket**, il n'y a pas de statut : l'erreur devient un **code de fermeture** RFC 6455
|
|
504
|
-
(`renderWebsocket()`, `error-renderer.ts:
|
|
521
|
+
(`renderWebsocket()`, `error-renderer.ts:518`) — 401/403 → 1008 (violation de politique),
|
|
505
522
|
5xx → 1011 (erreur interne), le reste → 4004 (plage privée). Si la socket n'est pas encore acceptée,
|
|
506
523
|
c'est un **rejet** de handshake.
|
|
507
524
|
|
|
508
525
|
> [!NOTE]
|
|
509
526
|
> Les erreurs de ton action remontent **seules** : le Resolver n'enveloppe pas l'appel dans un
|
|
510
|
-
> `try/catch` inutile (`Resolver.ts:
|
|
527
|
+
> `try/catch` inutile (`Resolver.ts:493-494`). Inutile d'attraper pour re-lever — sauf si tu veux
|
|
511
528
|
> vraiment traduire l'erreur en un autre statut.
|
|
512
529
|
|
|
513
530
|
## 🧩 Services injectés — trois façons
|
|
@@ -521,7 +538,7 @@ explicite :
|
|
|
521
538
|
const catalog = this.get<CatalogService>("catalog"); // null si absent ou container nettoyé
|
|
522
539
|
```
|
|
523
540
|
|
|
524
|
-
`Service.get()` (`Service.ts:
|
|
541
|
+
`Service.get()` (`Service.ts:525`) est une **façade sûre** : elle retourne `null` au lieu de lever si
|
|
525
542
|
le container a déjà été détaché. C'est le style à privilégier dans `initialize()`.
|
|
526
543
|
|
|
527
544
|
### 2. Injection par le constructeur — `@inject`
|
|
@@ -541,7 +558,7 @@ class DemoController extends Controller {
|
|
|
541
558
|
```
|
|
542
559
|
|
|
543
560
|
L'injecteur lit les noms déclarés par `@inject` et résout chaque dépendance dans le container avant
|
|
544
|
-
de construire (`Injector._instantiateWithStack()`, `injector.ts:
|
|
561
|
+
de construire (`Injector._instantiateWithStack()`, `injector.ts:549`). Le **contexte n'est pas une
|
|
545
562
|
dépendance** : il est passé en argument par le Resolver, et les paramètres non annotés le reçoivent
|
|
546
563
|
dans l'ordre (`injector.ts:309`). Les **dépendances circulaires sont détectées** et signalées avec le
|
|
547
564
|
chemin complet (`injector.ts:262-266`), jamais silencieusement.
|
|
@@ -568,9 +585,9 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
568
585
|
(`Controller.ts:574`).
|
|
569
586
|
- **Métadonnées d'action figées** : `@HttpCode`, `@Header`, les paramètres décorés et l'intention de
|
|
570
587
|
session sont calculés **une fois** par route puis mémorisés, au lieu d'être relus par `Reflect` à
|
|
571
|
-
chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:
|
|
588
|
+
chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:491`).
|
|
572
589
|
- **Gardes payées seulement si présentes** : sans `@IsGranted`, la vérification d'autorisation est
|
|
573
|
-
un test de nullité (`Resolver.ts:
|
|
590
|
+
un test de nullité (`Resolver.ts:406`) — 0 lookup, 0 `await`, 0 allocation.
|
|
574
591
|
- **Ta part du contrat** : pas de structure allouée « au cas où » dans le constructeur ni dans
|
|
575
592
|
`initialize()`. Une valeur utile à 5 % des requêtes s'alloue à la demande.
|
|
576
593
|
|
|
@@ -578,13 +595,13 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
578
595
|
|
|
579
596
|
| Domaine | Norme | Comment le code s'y conforme |
|
|
580
597
|
| -------------------------------- | ------------------------ | -------------------------------------------------------------- |
|
|
581
|
-
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:
|
|
598
|
+
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:1002`) |
|
|
582
599
|
| Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:107`) |
|
|
583
|
-
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:
|
|
584
|
-
| Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:
|
|
585
|
-
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:
|
|
600
|
+
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:503`) |
|
|
601
|
+
| Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:666`) |
|
|
602
|
+
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:936`), vérifié par le banc `auto-json` |
|
|
586
603
|
| Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts:734`) |
|
|
587
|
-
| Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:
|
|
604
|
+
| Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:518`) |
|
|
588
605
|
|
|
589
606
|
## 📡 Observabilité — Studio
|
|
590
607
|
|
|
@@ -601,16 +618,17 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
601
618
|
<!-- prettier-ignore -->
|
|
602
619
|
| Symptôme | Cause (dans le code) | Correction |
|
|
603
620
|
| --- | --- | --- |
|
|
604
|
-
| La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:
|
|
605
|
-
| Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:
|
|
621
|
+
| La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:981`) | Retourner une valeur, ou poser `@HttpCode(204)` |
|
|
622
|
+
| Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:951`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
|
|
606
623
|
| `Route Action not found` | L'action porte un nom déjà utilisé par un membre de `Controller` | Renommer : `session`, `request`, `response`, `context`, `route`, `method`, `query*`, `get`, `set`, `render*`, `redirect`, `forward` sont réservés |
|
|
607
|
-
| `this.session` est `null` dans `initialize()` | La session est activée **après** (`http-kernel.ts:
|
|
608
|
-
| Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:
|
|
624
|
+
| `this.session` est `null` dans `initialize()` | La session est activée **après** (`http-kernel.ts:1210`) | Lire la session dans l'action, pas dans le hook |
|
|
625
|
+
| Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1715`) | Déplacer l'effet de bord dans l'action |
|
|
609
626
|
| Redirection permanente non voulue | Un statut invalide retombe sur 302, un `301` explicite reste 301 | Passer le code voulu : `this.redirect(url, 302)` |
|
|
610
|
-
| WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée par toute la connexion (`Resolver.ts:
|
|
627
|
+
| WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée — par TOUTES les connexions en singleton (le défaut), par toute la connexion en `@Scope("request")` (`Resolver.ts:805`) | Porter l'état par message, ou sur le contexte de la connexion |
|
|
611
628
|
| WS : l'action n'est jamais appelée | Route sans transport `WEBSOCKET` déclaré | `requirements: { methods: ["WEBSOCKET"] }` |
|
|
612
|
-
| Contrôleur
|
|
613
|
-
|
|
|
629
|
+
| Contrôleur singleton : données d'un autre utilisateur | État muté que les filets ne voient pas (`#privé`, objet muté) sur l'instance partagée | Passer par les arguments décorés, ou déclarer `@Scope("request")` |
|
|
630
|
+
| « Contrôleur … : écriture de « this.x » refusée » | Un état de requête écrit sur un singleton (le défaut) | Argument décoré, service `request`, ou `@Scope("request")` sur la classe |
|
|
631
|
+
| Event loop figé sur une route de fichier | `getFile()` synchrone (`lstatSync`, `Controller.ts:682`) | Utiliser `getFileAsync()` (`Controller.ts:722`) |
|
|
614
632
|
|
|
615
633
|
## 🧪 Tests & couverture
|
|
616
634
|
|