@nodefony/realtime 10.0.0-beta.3 → 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.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +4 -6
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/configSchema.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +1 -2
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +1 -2
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +0 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
- package/dist/nodefony/src/guards.js +1 -2
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +154 -80
- package/dist/nodefony/src/server/RealtimeHub.js +3 -3
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +1 -2
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +1 -2
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +77 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +58 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +6 -1
- package/docs/actions.md +44 -43
- package/docs/architecture.md +125 -119
- package/docs/cadence-et-contre-pression.md +31 -22
- package/docs/configuration.md +106 -82
- package/docs/cookbook-chat.md +37 -34
- package/docs/index.md +35 -24
- package/docs/observabilite.md +49 -49
- package/docs/protocole.md +91 -68
- package/docs/securite.md +125 -114
- package/docs/vocabulaire.md +117 -114
- package/package.json +10 -10
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -6,7 +6,7 @@ import type { IRealtimeConnProbe } from "../../interfaces/IRealtimeProbe.js";
|
|
|
6
6
|
* `ctx.connection` (WebsocketContext) la fournit.
|
|
7
7
|
*/
|
|
8
8
|
export interface RawWsConnection {
|
|
9
|
-
send(data: string, cb?: (err?: Error) => void): void;
|
|
9
|
+
send(data: string | ArrayBuffer | ArrayBufferView, cb?: (err?: Error) => void): void;
|
|
10
10
|
close(code?: number, reason?: string): void;
|
|
11
11
|
readonly readyState: number;
|
|
12
12
|
/**
|
|
@@ -46,6 +46,8 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
|
|
|
46
46
|
private _bytesSent;
|
|
47
47
|
private _messagesSent;
|
|
48
48
|
private _dropped;
|
|
49
|
+
private _binaryDiscarded;
|
|
50
|
+
private _malformedDiscarded;
|
|
49
51
|
private readonly _max;
|
|
50
52
|
private readonly _policy;
|
|
51
53
|
private readonly _closeAfterDrops;
|
|
@@ -56,6 +58,29 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
|
|
|
56
58
|
});
|
|
57
59
|
connect(): void;
|
|
58
60
|
send(raw: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Émet des octets tels quels — trame binaire RFC 6455, hors JSON-RPC (#617).
|
|
63
|
+
* Même contre-pression (`decideSend`, la règle unique) et mêmes compteurs que
|
|
64
|
+
* {@link send}. Écrite à part, et non en factorisant `send` : `send` est le
|
|
65
|
+
* chemin de CHAQUE frame JSON-RPC, et n'y paie pas un appel de plus.
|
|
66
|
+
*
|
|
67
|
+
* @param data - les octets à émettre.
|
|
68
|
+
* @returns `true` si la trame est partie, `false` si la connexion était
|
|
69
|
+
* fermée ou la contre-pression l'a jetée.
|
|
70
|
+
*/
|
|
71
|
+
sendBinary(data: Uint8Array): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Compte une trame ENTRANTE écartée et rend le cumul de sa nature — `1` dit
|
|
74
|
+
* « première », le seul instant où l'appelant journalise.
|
|
75
|
+
*
|
|
76
|
+
* @param kind - `binary` (sans lecteur) ou `malformed` (texte non JSON).
|
|
77
|
+
* @returns le cumul de cette nature, après incrément.
|
|
78
|
+
*/
|
|
79
|
+
discard(kind: "binary" | "malformed"): number;
|
|
80
|
+
/** Cumul de trames BINAIRES reçues et écartées faute de lecteur — monotone. */
|
|
81
|
+
get binaryDiscarded(): number;
|
|
82
|
+
/** Cumul de trames TEXTE reçues et écartées parce que non JSON — monotone. */
|
|
83
|
+
get malformedDiscarded(): number;
|
|
59
84
|
close(code?: number, reason?: string): void;
|
|
60
85
|
get readyState(): number;
|
|
61
86
|
/** Octets en file d'envoi non drainés (`ws.bufferedAmount`) — risque #1. `0` si mock. */
|
|
@@ -130,7 +130,10 @@ export interface IRealtimeHarness<C extends RealtimeController> {
|
|
|
130
130
|
* échec) : lire `closes` pour en connaître la cause.
|
|
131
131
|
*/
|
|
132
132
|
connect(): Promise<IRealtimeFrame>;
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Demande un canal, comme le ferait un client, et attend la réponse du
|
|
135
|
+
* serveur (acquittement `realtime:subscribed` ou refus `realtime:denied`).
|
|
136
|
+
*/
|
|
134
137
|
subscribe(channel: string): Promise<void>;
|
|
135
138
|
/** Rend un canal — le provider est disposé au dernier abonné. */
|
|
136
139
|
unsubscribe(channel: string): Promise<void>;
|
|
@@ -149,6 +152,8 @@ export interface IRealtimeHarness<C extends RealtimeController> {
|
|
|
149
152
|
messages(channel: string): readonly unknown[];
|
|
150
153
|
/** Les refus d'abonnement notifiés (`realtime:denied`). */
|
|
151
154
|
denials(): readonly IHarnessDenied[];
|
|
155
|
+
/** Les canaux dont l'abonnement a été acquitté (`realtime:subscribed`), dans l'ordre. */
|
|
156
|
+
subscriptions(): readonly string[];
|
|
152
157
|
/** Ferme la socket côté client : la base dispose alors tous ses providers. */
|
|
153
158
|
close(): void;
|
|
154
159
|
/** `close()` puis remise à zéro du hub. À appeler en fin de cas. */
|
package/docs/actions.md
CHANGED
|
@@ -117,7 +117,7 @@ attendre trop longtemps et repartir.
|
|
|
117
117
|
|
|
118
118
|
Techniquement, ce numéro est le champ `id` de la frame JSON-RPC 2.0. Le pair l'attribue, garde
|
|
119
119
|
l'appel en attente dans une table, arme une minuterie, et résout la `Promise` quand la réponse
|
|
120
|
-
portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts
|
|
120
|
+
portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts#JsonRpcPeer.handleResponse`). Une frame sans
|
|
121
121
|
`id` ne crée aucune de ces trois choses — c'est pourquoi une publication ne coûte rien au repos.
|
|
122
122
|
|
|
123
123
|
## La vision Nodefony — un nom, un handler, une découverte
|
|
@@ -126,18 +126,18 @@ Trois partis pris distinguent les actions Nodefony d'une couche RPC classique.
|
|
|
126
126
|
|
|
127
127
|
**Il n'y a pas de couche RPC séparée.** Une action est une **méthode de contrôleur**, dans la même
|
|
128
128
|
classe que tes canaux et — si tu le veux — que tes routes HTTP. Pas de service dédié, pas de
|
|
129
|
-
schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts
|
|
129
|
+
schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts#RealtimeAction`) suffit, et le
|
|
130
130
|
retour de la méthode devient le `result` de la réponse.
|
|
131
131
|
|
|
132
132
|
**Le contrat du handler est minuscule, volontairement.** Une action reçoit **un seul argument**,
|
|
133
133
|
les paramètres bruts du client, et rend une valeur — `RpcActionHandler`
|
|
134
|
-
(`JsonRpcPeer.ts
|
|
134
|
+
(`JsonRpcPeer.ts#RpcActionHandler`). Pas de contexte injecté dans la signature : le `this` est lié à
|
|
135
135
|
l'instance du contrôleur au handshake, ce qui donne accès au noyau, aux services et à la
|
|
136
136
|
connexion sans élargir le contrat.
|
|
137
137
|
|
|
138
138
|
**L'endpoint s'annonce lui-même.** La liste des actions exposées voyage dans la frame d'accueil —
|
|
139
|
-
`IRealtimeWelcome` (`RealtimeController.ts
|
|
140
|
-
`NodefonySocket.serverMethods` (`NodefonySocket.ts
|
|
139
|
+
`IRealtimeWelcome` (`RealtimeController.ts`) — et se lit côté client par
|
|
140
|
+
`NodefonySocket.serverMethods` (`NodefonySocket.ts#NodefonySocket.serverMethods`). Une interface n'écrit donc jamais un nom d'action en dur : elle
|
|
141
141
|
n'active un bouton que si le serveur a déclaré savoir le servir.
|
|
142
142
|
|
|
143
143
|
**Le compromis, dit franchement** : une action est **un aller-retour**, point. Elle ne diffuse pas,
|
|
@@ -274,20 +274,20 @@ Le trajet complet d'une requête, des étapes qu'elle traverse aux branches par
|
|
|
274
274
|
|
|
275
275
|
### Deux voies, un même registre
|
|
276
276
|
|
|
277
|
-
| Voie
|
|
278
|
-
|
|
|
279
|
-
| `@RealtimeAction("nom")` (`realtimeDecorators.ts
|
|
280
|
-
| `realtimeActions()` (`RealtimeController.ts
|
|
277
|
+
| Voie | Quand la choisir |
|
|
278
|
+
| -------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
279
|
+
| `@RealtimeAction("nom")` (`realtimeDecorators.ts#RealtimeAction`) | le cas courant — un nom fixe, une méthode, déclaratif |
|
|
280
|
+
| `realtimeActions()` (`RealtimeController.ts#RealtimeController.realtimeActions`) | la table est **calculée** (noms dynamiques, boucle, config) |
|
|
281
281
|
|
|
282
282
|
Les deux sont fusionnées au handshake, et **l'override gagne** en cas de conflit de nom : une
|
|
283
283
|
classe peut ainsi remplacer une action héritée sans toucher au parent
|
|
284
|
-
(`RealtimeController.ts
|
|
284
|
+
(`RealtimeController.ts#RealtimeController.onHandshake`). Un endpoint sans aucune action ne paie rien — la table du pair est
|
|
285
285
|
allouée au premier enregistrement seulement.
|
|
286
286
|
|
|
287
287
|
### Ce que ton `return` et ton `throw` deviennent sur le fil
|
|
288
288
|
|
|
289
289
|
Le retour du handler est envoyé tel quel en `result`. Les erreurs, elles, suivent une règle Zero
|
|
290
|
-
Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts
|
|
290
|
+
Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) :
|
|
291
291
|
|
|
292
292
|
| Côté serveur | Ce que reçoit le client | Pourquoi |
|
|
293
293
|
| ------------------------------- | ---------------------------------------- | ----------------------------------------------- |
|
|
@@ -299,7 +299,7 @@ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:52
|
|
|
299
299
|
|
|
300
300
|
> [!WARNING]
|
|
301
301
|
> **Le temps réel n'a qu'UNE erreur, et elle vient du cœur.** Pour choisir ce que voit le client,
|
|
302
|
-
> c'est `RpcError` (`JsonRpcPeer.ts
|
|
302
|
+
> c'est `RpcError` (`JsonRpcPeer.ts#RpcError`), importée depuis `nodefony` — son `code` est un entier
|
|
303
303
|
> JSON-RPC et son `data` traverse le fil. Toute autre exception donne un `-32603` opaque. Ne pas
|
|
304
304
|
> chercher d'erreur maison dans `@nodefony/realtime` : il n'y en a pas, et
|
|
305
305
|
> `tests/unit/errorSurface.test.ts` refuse d'en exporter une que personne ne lève.
|
|
@@ -313,8 +313,8 @@ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:52
|
|
|
313
313
|
|
|
314
314
|
Cette défense est le **verrou de frame**, posé par `@nodefony/security`. Il examine chaque frame
|
|
315
315
|
entrante et, pour une action, résout une politique **par son nom** — exactement le mécanisme des
|
|
316
|
-
canaux (`buildFrameAuthorizer()`, `frameAuthorizer.ts
|
|
317
|
-
`frameAuthorizer.ts
|
|
316
|
+
canaux (`buildFrameAuthorizer()`, `frameAuthorizer.ts#buildFrameAuthorizer` ; branche des méthodes,
|
|
317
|
+
`frameAuthorizer.ts#buildFrameAuthorizer`). Trois situations, à connaître dans cet ordre :
|
|
318
318
|
|
|
319
319
|
### Situation 1 — une action est FERMÉE par défaut
|
|
320
320
|
|
|
@@ -345,22 +345,22 @@ homonyme), elle est conservée : le défaut ne rétrograde jamais une règle plu
|
|
|
345
345
|
### Situation 2 — les namespaces réservés portent un plancher
|
|
346
346
|
|
|
347
347
|
Un nom qui commence par `nodefony:` hérite d'un plancher non contournable : authentifié **et**
|
|
348
|
-
`ROLE_NODEFONY_ADMIN` (`SYSTEM_CHANNEL_POLICY`, `frameAuthorizer.ts
|
|
348
|
+
`ROLE_NODEFONY_ADMIN` (`SYSTEM_CHANNEL_POLICY`, `frameAuthorizer.ts#SYSTEM_CHANNEL_POLICY`) — un rôle de la plateforme,
|
|
349
349
|
pas le `ROLE_ADMIN` d'une organisation. Un seul préfixe couvre donc tout
|
|
350
|
-
ce que la plateforme expose (`DEFAULT_SYSTEM_PREFIXES`, `frameAuthorizer.ts
|
|
350
|
+
ce que la plateforme expose (`DEFAULT_SYSTEM_PREFIXES`, `frameAuthorizer.ts#DEFAULT_SYSTEM_PREFIXES`). C'est ce qui
|
|
351
351
|
protège `nodefony:kernel:gc` : le nom **est** la garde, et la comparaison est insensible à la casse
|
|
352
352
|
pour qu'un `NODEFONY:kernel:gc` ne passe pas à côté.
|
|
353
353
|
|
|
354
354
|
Ce plancher **prime sur ta déclaration** — y compris sur `{ authenticated: false }`. Et il ne
|
|
355
355
|
s'arrête pas aux préfixes : tout nom **contenant** `:health` ou `:stats` est traité comme un canal
|
|
356
|
-
d'observabilité (`matchSystemPolicy`, `frameAuthorizer.ts
|
|
356
|
+
d'observabilité (`matchSystemPolicy`, `frameAuthorizer.ts#matchSystemPolicy`). Une action `public:health` reste
|
|
357
357
|
donc réservée, quoi qu'en dise son décorateur. À savoir avant de nommer une action « ouverte ».
|
|
358
358
|
|
|
359
359
|
### Situation 3 — couvrir SES actions par la configuration
|
|
360
360
|
|
|
361
361
|
Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
|
|
362
362
|
configuration de sécurité — la même liste que pour les canaux
|
|
363
|
-
(`realtimeChannels`, `security/nodefony/config/config.ts
|
|
363
|
+
(`realtimeChannels`, `security/nodefony/config/config.ts#securityConfigSchema`) :
|
|
364
364
|
|
|
365
365
|
```ts ignore
|
|
366
366
|
use("@nodefony/security", {
|
|
@@ -376,7 +376,7 @@ use("@nodefony/security", {
|
|
|
376
376
|
|
|
377
377
|
Une action authentifiée sait que l'appelant est identifié ; elle peut aussi savoir **qui** il est.
|
|
378
378
|
Les décorateurs de paramètres d'une route y prennent le même sens
|
|
379
|
-
(`actionParamsWrapper`, `RealtimeController.ts
|
|
379
|
+
(`actionParamsWrapper`, `RealtimeController.ts#RealtimeController.actionParamsWrapper`) :
|
|
380
380
|
|
|
381
381
|
```ts ignore
|
|
382
382
|
import { Body, CurrentUser } from "@nodefony/framework";
|
|
@@ -402,8 +402,8 @@ rien ne change pour elle, ni comportement ni coût.
|
|
|
402
402
|
|
|
403
403
|
> [!IMPORTANT]
|
|
404
404
|
> **L'identité est celle de l'ouverture de la socket.** Elle n'est pas relue à chaque appel : le
|
|
405
|
-
> hub la revalide toutes les 30 secondes (`REVOCATION_REVALIDATE_MS`, `RealtimeHub.ts
|
|
406
|
-
> ferme la socket en `4001` si la session est morte (`RealtimeHub.ts
|
|
405
|
+
> hub la revalide toutes les 30 secondes (`REVOCATION_REVALIDATE_MS`, `RealtimeHub.ts#channelFamily`) et
|
|
406
|
+
> ferme la socket en `4001` si la session est morte (`RealtimeHub.ts#RealtimeHub.probe`). Après une
|
|
407
407
|
> déconnexion, une action peut donc encore voir l'ancien utilisateur pendant cette fenêtre. Pour
|
|
408
408
|
> une écriture sensible — paiement, droits, suppression — passe par une route
|
|
409
409
|
> `POST + WEBSOCKET` appelée par `socket.mutate()` : ce pont revalide l'identité à **chaque**
|
|
@@ -414,7 +414,7 @@ rien ne change pour elle, ni comportement ni coût.
|
|
|
414
414
|
Une action réservée au développement se contrôle **dans le handler, côté serveur** — jamais par un
|
|
415
415
|
drapeau d'interface. Cacher un bouton n'empêche personne de forger la frame. Le modèle est
|
|
416
416
|
`nodefony:scaffold:run` dans Studio, qui refuse net hors développement avant même de lire ses paramètres
|
|
417
|
-
(`StudioRealtimeController.ts
|
|
417
|
+
(`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`).
|
|
418
418
|
|
|
419
419
|
> [!CAUTION]
|
|
420
420
|
> Le défaut fermé protège de l'oubli, pas de la sur-ouverture. Une action mutable reste la surface
|
|
@@ -428,21 +428,21 @@ drapeau d'interface. Cacher un bouton n'empêche personne de forger la frame. Le
|
|
|
428
428
|
### Le délai d'expiration est la seule libération automatique
|
|
429
429
|
|
|
430
430
|
`request()` prend le délai en **troisième argument positionnel**, en millisecondes — il n'y a pas
|
|
431
|
-
d'objet d'options (`NodefonySocket.request()`, `NodefonySocket.ts
|
|
431
|
+
d'objet d'options (`NodefonySocket.request()`, `NodefonySocket.ts#NodefonySocket.request`) :
|
|
432
432
|
|
|
433
433
|
```ts ignore
|
|
434
434
|
await socket.request("orders:quote", { orderId }); // 30 000 ms par défaut
|
|
435
435
|
await socket.request("orders:export", { scope }, 120_000); // action longue
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
| Défaut | Valeur | Ancrage
|
|
439
|
-
| --------- | --------- |
|
|
440
|
-
| `request` | 30 000 ms | `JsonRpcPeer.ts
|
|
438
|
+
| Défaut | Valeur | Ancrage |
|
|
439
|
+
| --------- | --------- | ------------------------------------ |
|
|
440
|
+
| `request` | 30 000 ms | `JsonRpcPeer.ts#JsonRpcPeer.request` |
|
|
441
441
|
|
|
442
442
|
À l'expiration, l'entrée en attente est **retirée** et la `Promise` rejetée avec
|
|
443
|
-
`RPC timeout: <méthode>` (`JsonRpcPeer.ts
|
|
443
|
+
`RPC timeout: <méthode>` (`JsonRpcPeer.ts#JsonRpcPeer.startCall`). Conséquence à connaître : une réponse qui
|
|
444
444
|
arriverait après coup ne trouve plus personne et est ignorée en silence
|
|
445
|
-
(`JsonRpcPeer.ts
|
|
445
|
+
(`JsonRpcPeer.ts#JsonRpcPeer.handleResponse`) — pas de résolution tardive, pas de fuite.
|
|
446
446
|
|
|
447
447
|
### Abandonner : ce qui existe vraiment
|
|
448
448
|
|
|
@@ -456,11 +456,11 @@ Trois leviers existent, et ils ne font pas la même chose :
|
|
|
456
456
|
|
|
457
457
|
1. **Le délai d'expiration** — libère le client, laisse le serveur travailler.
|
|
458
458
|
2. **La fermeture de la connexion** — `dispose()` rejette **tous** les appels en attente d'un
|
|
459
|
-
coup (`JsonRpcPeer.ts
|
|
460
|
-
(`RealtimeController.ts
|
|
459
|
+
coup (`JsonRpcPeer.ts#JsonRpcPeer.dispose`), appelé au nettoyage de la socket
|
|
460
|
+
(`RealtimeController.ts#RealtimeController.onHandshake`). Là encore : côté client seulement.
|
|
461
461
|
3. **Une action compagnon** — la seule vraie annulation. On expose une seconde action qui prend
|
|
462
462
|
l'identifiant du travail et l'interrompt côté serveur. Le modèle du dépôt est
|
|
463
|
-
`nodefony:scaffold:cancel` (`StudioRealtimeController.ts
|
|
463
|
+
`nodefony:scaffold:cancel` (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`), pendant de `nodefony:scaffold:run`.
|
|
464
464
|
|
|
465
465
|
### Rejouer : idempotence par action
|
|
466
466
|
|
|
@@ -478,8 +478,8 @@ fois ? »**.
|
|
|
478
478
|
| une mutation (`socket.mutate`) | **non** par nature | oui, **avec une clé d'idempotence** |
|
|
479
479
|
|
|
480
480
|
Pour les mutations passant par le pont API, la clé n'est pas une convention : elle est **exigée
|
|
481
|
-
par la signature** de `mutate()` (`NodefonySocket.ts
|
|
482
|
-
(`routerDecorators.ts
|
|
481
|
+
par la signature** de `mutate()` (`NodefonySocket.ts#NodefonySocket.mutate`), et c'est la garde `@Idempotent`
|
|
482
|
+
(`routerDecorators.ts#Idempotent`) qui, côté serveur, reconnaît le rejeu et rend la réponse déjà calculée
|
|
483
483
|
au lieu de refaire l'effet.
|
|
484
484
|
|
|
485
485
|
```ts ignore
|
|
@@ -505,11 +505,11 @@ contraire des résultats **au fil de l'eau**, pendant des minutes.
|
|
|
505
505
|
Le remède tient en trois gestes, et il est déjà en production dans Studio :
|
|
506
506
|
|
|
507
507
|
1. **L'action lance et rend un identifiant**, sans attendre — `nodefony:scaffold:run`
|
|
508
|
-
(`StudioRealtimeController.ts
|
|
508
|
+
(`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`) démarre le travail et rend son état, dont son `id`.
|
|
509
509
|
2. **Le flux se diffuse sur un canal dédié**, nommé d'après ce travail — `nodefony:scaffold:job@<id>`,
|
|
510
|
-
servi par le contrôleur (`StudioRealtimeController.ts
|
|
510
|
+
servi par le contrôleur (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldCancel`).
|
|
511
511
|
3. **Le client s'abonne à ce canal** et regarde le travail se faire, ligne après ligne
|
|
512
|
-
(`Create.tsx
|
|
512
|
+
(`Create.tsx#Create`).
|
|
513
513
|
|
|
514
514
|
### La course, et comment le dépôt la neutralise
|
|
515
515
|
|
|
@@ -519,7 +519,7 @@ prudente est donc de **s'abonner avant** de lancer l'appel.
|
|
|
519
519
|
|
|
520
520
|
Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
|
|
521
521
|
canal **rejoue son historique** au nouvel abonné, de sorte qu'un arrivant tardif voit tout depuis
|
|
522
|
-
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts
|
|
522
|
+
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts#ScaffoldService.subscribe`). C'est ce qui autorise le front
|
|
523
523
|
à faire l'appel d'abord et à s'abonner ensuite, sans rien perdre.
|
|
524
524
|
|
|
525
525
|
> [!TIP]
|
|
@@ -529,7 +529,7 @@ le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qu
|
|
|
529
529
|
|
|
530
530
|
### Et le streaming du protocole ?
|
|
531
531
|
|
|
532
|
-
**Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts
|
|
532
|
+
**Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts#RpcActionHandler`), que
|
|
533
533
|
le pair emballe en une réponse unique. Pour tout ce qui progresse — une réponse mot à mot d'un
|
|
534
534
|
modèle de langage, un export qui avance, un traitement long — la voie est le motif
|
|
535
535
|
**« travail + canal »** : l'action accuse réception, et la progression arrive sur un canal.
|
|
@@ -544,15 +544,15 @@ il sera conçu avec son premier consommateur réel.
|
|
|
544
544
|
Un cas particulier mérite d'être connu avant d'écrire une action : **elle existe peut-être déjà en
|
|
545
545
|
HTTP**. Le pont API expose la méthode `api.request`, qui rejoue une route de contrôleur sur la
|
|
546
546
|
socket, avec la même garde et le même résultat qu'en REST — `invokeApiRequest()`
|
|
547
|
-
(`RealtimeController.ts
|
|
548
|
-
`realtimeApiRequest()` (`RealtimeController.ts
|
|
547
|
+
(`RealtimeController.ts#RealtimeController.invokeApiRequest`). Il est **désactivé par défaut** et s'active en surchargeant
|
|
548
|
+
`realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`).
|
|
549
549
|
|
|
550
550
|
```ts ignore
|
|
551
551
|
const modules = await socket.request("/nodefony/kernel/api/modules");
|
|
552
552
|
```
|
|
553
553
|
|
|
554
554
|
La forme se discrimine toute seule : un chemin commence par `/`, jamais un nom d'action —
|
|
555
|
-
`NodefonySocket.request()` (`NodefonySocket.ts
|
|
555
|
+
`NodefonySocket.request()` (`NodefonySocket.ts#NodefonySocket.request`). Écris une action RPC pour ce qui n'a de sens **que** sur la socket ;
|
|
556
556
|
passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit dans le
|
|
557
557
|
[vocabulaire](./vocabulaire.md) et l'[architecture](./architecture.md).
|
|
558
558
|
|
|
@@ -564,6 +564,7 @@ passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit
|
|
|
564
564
|
| Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
|
|
565
565
|
| `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
|
|
566
566
|
| `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
|
|
567
|
+
| `api.request` rejeté avec `data.status: 422` | la route invoquée valide son entrée (`schema`, ou une `ZodError` de l'action) | lire `data.fields` : chaque champ fautif, sa partie (`in`) et son message |
|
|
567
568
|
| `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
|
|
568
569
|
| Une action est refusée à un anonyme alors qu'on la croyait publique | une action est fermée par défaut (`{ authenticated: true }`) | déclarer `{ authenticated: false }` pour l'ouvrir ; un rôle se pose par une règle de préfixe |
|
|
569
570
|
| Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |
|
|
@@ -596,11 +597,11 @@ vivent dans la carte de l'aperçu, régénérée depuis les résultats réels, j
|
|
|
596
597
|
### Éprouver SON action — le harnais, identité comprise
|
|
597
598
|
|
|
598
599
|
Une application teste ses actions sans serveur ni navigateur avec `createRealtimeHarness`
|
|
599
|
-
(`@nodefony/realtime/testing`, `nodefony/testing/index.ts
|
|
600
|
+
(`@nodefony/realtime/testing`, `nodefony/testing/index.ts#createRealtimeHarness`) : il monte le contrôleur sur une
|
|
600
601
|
fausse connexion, envoie les frames qu'un client enverrait et rend celles qui sortent. Sans option,
|
|
601
602
|
la connexion est **anonyme** — le repli Zero Trust. Une action qui lit `@CurrentUser()` s'éprouve
|
|
602
603
|
donc avec l'identité qu'aurait posée le pare-feu au handshake, passée en `identity`
|
|
603
|
-
(`nodefony/testing/index.ts
|
|
604
|
+
(`nodefony/testing/index.ts#IRealtimeHarnessOptions.identity`) :
|
|
604
605
|
|
|
605
606
|
```ts ignore
|
|
606
607
|
import { describe, it, expect, afterEach } from "vitest";
|