Actions RPC — appeler le serveur et attendre sa réponse
stable@nodefony/realtimemis à jour 2026-07-20
Une action RPC est la direction « contrôle » de la socket : le client demande, le serveur exécute, le serveur répond — une
Promiserésolue avec le résultat. C’est l’exact opposé d’une publication, qui part sans accusé. Cette page dit quand choisir l’une plutôt que l’autre, comment déclarer une action, et ce qui arrive quand le client abandonne, quand le réseau coupe ou quand l’appel est rejoué.
📍 Documentation › Realtime › Actions RPC
🧠 Le modèle mental — deux directions sur un même fil#
Une seule connexion porte deux régimes de trafic. Ils ne se distinguent pas par leur contenu mais
par la présence d’un id dans la frame : un id réclame une réponse, son absence non.
Publication ou action RPC ? Le critère de tri#
| Aspect | Publication (sans id) |
Action RPC (avec id) |
|---|---|---|
| Direction | flux, 1 → N abonnés | requête → réponse, 1 → 1 |
| Sémantique | « ceci s’est passé » | « fais ceci, et dis-moi » |
| Latence | la livraison, sans attente | l’exécution complète du handler |
| Ordre | par canal | par appel, chacun corrélé par son id |
| Rejeu | inutile — une perte est rattrapée | possible, si l’action est idempotente |
| Coût au repos | un sink dans la table du hub | une entrée en attente + une minuterie par appel |
| Exemples | nodefony:socket, chat:room-42 |
nodefony:kernel:ping, nodefony:scaffold:run |
💡 Astuce
Le critère qui tranche en une seconde : le client a-t-il besoin de savoir si ça a marché ? Si oui, c’est une action RPC. Si non, c’est une publication. Une « action » dont personne ne lit le retour est une publication déguisée ; un « événement » dont l’émetteur attend confirmation est une action qui s’ignore.
📖 Lexique#
| Terme | Sens |
|---|---|
| Action RPC | Une méthode nommée, exposée par le serveur, appelable par le client et qui rend une valeur. |
| RPC | Remote Procedure Call : appeler une fonction qui vit ailleurs comme si elle était locale. |
| Frame | L’unité qui passe sur le fil, au format JSON-RPC 2.0. |
id |
Le numéro qui corrèle une requête et sa réponse. Sa présence définit une requête. |
| Corrélation | Retrouver, à l’arrivée d’une réponse, l’appel en attente qu’elle résout. |
| Accueil (welcome) | La première frame du serveur ; son champ methods liste les actions découvrables. |
| Découverte | Lire ce que l’endpoint expose au lieu de l’écrire en dur dans l’interface. |
| Délai d’expiration | Le temps au bout duquel le client cesse d’attendre et rejette sa Promise. |
| Idempotence | Propriété d’une action dont deux exécutions font le même effet qu’une seule. |
| Clé d’idempotence | L’identifiant fourni par l’appelant qui permet au serveur de reconnaître un rejeu. |
| Verrou de frame | La décision synchrone « cette frame passe-t-elle ? », posée par la couche sécurité. |
| Namespace réservé | Le préfixe nodefony:, qui marque les surfaces de la plateforme et porte un plancher non contournable. |
| Pont API | La méthode api.request qui rejoue une route HTTP sur la socket. |
| Job | Un travail long identifié, lancé par une action et suivi sur un canal dédié. |
Les mots communs à tout le module (socket, canal, hub, pair, backplane) sont définis une seule fois, dans le vocabulaire.
Qu’est-ce qu’une action RPC ?#
Imagine un guichet et un panneau d’affichage dans le même hall.
Le panneau d’affichage, c’est la publication : il change, tout le monde le voit, personne ne signe. Si tu regardais ailleurs, tu as raté l’information — et ce n’est pas grave, la suivante arrive.
Le guichet, c’est l’action RPC : tu tends un formulaire portant un numéro, tu attends, on te rend une réponse portant le même numéro. Tu sais si ça a marché. Tu peux échouer. Tu peux attendre trop longtemps et repartir.
Techniquement, ce numéro est le champ id de la frame JSON-RPC 2.0. Le pair l’attribue, garde
l’appel en attente dans une table, arme une minuterie, et résout la Promise quand la réponse
portant cet id revient (JsonRpcPeer.handleResponse(), JsonRpcPeer.ts:556). Une frame sans
id ne crée aucune de ces trois choses — c’est pourquoi une publication ne coûte rien au repos.
La vision Nodefony — un nom, un handler, une découverte#
Trois partis pris distinguent les actions Nodefony d’une couche RPC classique.
Il n’y a pas de couche RPC séparée. Une action est une méthode de contrôleur, dans la même
classe que tes canaux et — si tu le veux — que tes routes HTTP. Pas de service dédié, pas de
schéma à compiler : @RealtimeAction("orders:quote") (realtimeDecorators.ts:101) suffit, et le
retour de la méthode devient le result de la réponse.
Le contrat du handler est minuscule, volontairement. Une action reçoit un seul argument,
les paramètres bruts du client, et rend une valeur — RpcActionHandler
(JsonRpcPeer.ts:118). Pas de contexte injecté dans la signature : le this est lié à
l’instance du contrôleur au handshake, ce qui donne accès au noyau, aux services et à la
connexion sans élargir le contrat.
L’endpoint s’annonce lui-même. La liste des actions exposées voyage dans la frame d’accueil —
IRealtimeWelcome (RealtimeController.ts:8) — et se lit côté client par
RealtimeClient.serverMethods (RealtimeClient.ts:591). Une interface n’écrit donc jamais un nom d’action en dur : elle
n’active un bouton que si le serveur a déclaré savoir le servir.
Le compromis, dit franchement : une action est un aller-retour, point. Elle ne diffuse pas, elle ne progresse pas, elle ne s’annule pas côté serveur. Tout ce qui dure ou se raconte en plusieurs temps se fait sur un canal — voir plus bas.
🚀 Démarrage rapide#
Une action qui calcule un devis, vue d’une application créée par nodefony create app.
1. Le contrôleur — la seule chose à écrire côté serveur#
// nodefony/controllers/OrdersRealtimeController.ts
import { controller, route } from "@nodefony/framework";
import { RealtimeController, RealtimeAction } from "@nodefony/realtime";
import type { ContextType } from "@nodefony/http";
import { RpcError } from "nodefony";
interface IQuote {
orderId: string;
total: number;
currency: string;
}
@controller("/orders")
class OrdersRealtimeController extends RealtimeController {
constructor(context: ContextType) {
super("orders", context);
}
// UNE route WebSocket : la classe de base porte tout le protocole.
@route("orders-realtime", {
path: "/realtime",
requirements: { methods: ["WEBSOCKET"] },
})
async realtime(message: string | Buffer | null): Promise<void> {
this.handleRealtime(message);
}
// L'ACTION : un seul argument (les params du client, JAMAIS fiables),
// et la valeur rendue devient le `result` de la réponse.
@RealtimeAction("orders:quote")
quote(params: unknown): IQuote {
const orderId = (params as { orderId?: unknown } | undefined)?.orderId;
// Une RpcError est la SEULE erreur dont le message atteint le client.
// Tout autre throw devient un `-32603 internal error` opaque.
if (typeof orderId !== "string") {
throw new RpcError("params.orderId manquant", -32602);
}
return { orderId, total: 4200, currency: "EUR" };
}
}
export default OrdersRealtimeController;
2. L’appel — côté navigateur#
// frontend/src/orders.ts
import { RealtimeClient } from "nodefony/client";
interface IQuote {
orderId: string;
total: number;
currency: string;
}
const socket = RealtimeClient.shared({
url: "wss://127.0.0.1:5152/orders/realtime",
});
export async function askQuote(orderId: string): Promise<IQuote | null> {
await socket.connect();
// DÉCOUVERTE : le serveur a-t-il annoncé cette action ? Sinon, on n'appelle
// pas — c'est ainsi qu'un bouton s'active au lieu d'être écrit en dur.
if (!socket.serverMethods?.includes("orders:quote")) return null;
// Signature POSITIONNELLE : (méthode, params, délai en ms). Défaut 30 000.
return socket.request<"orders:quote", IQuote>(
"orders:quote",
{ orderId },
5000,
);
}
Ce qu’on observe#
À la connexion, la frame d’accueil annonce l’action — c’est elle qui pilote l’interface :
{
"jsonrpc": "2.0",
"method": "realtime:welcome",
"params": {
"ts": 1770000000000,
"protocol": "jsonrpc-2.0",
"channels": [],
"methods": ["orders:quote"],
"identity": {
"type": "anonymous",
"authenticated": false,
"userIdentifier": "anonymous",
"roles": ["ROLE_ANONYMOUS"],
"scopes": [],
},
},
}
Puis l’aller-retour, deux frames, corrélées par leur id :
// → client vers serveur
{ "jsonrpc": "2.0", "id": 1, "method": "orders:quote", "params": { "orderId": "A-42" } }
// ← serveur vers client
{ "jsonrpc": "2.0", "id": 1, "result": { "orderId": "A-42", "total": 4200, "currency": "EUR" } }
Un orderId absent donne l’autre forme de réponse, l’échec explicite :
{
"jsonrpc": "2.0",
"id": 2,
"error": { "code": -32602, "message": "params.orderId manquant" },
}
Le trajet complet d’une requête, des étapes qu’elle traverse aux branches par lesquelles elle
🧰 Déclarer une action côté serveur#
Deux voies, un même registre#
| Voie | Quand la choisir |
|---|---|
@RealtimeAction("nom") (realtimeDecorators.ts:101) |
le cas courant — un nom fixe, une méthode, déclaratif |
realtimeActions() (RealtimeController.ts:179) |
la table est calculée (noms dynamiques, boucle, config) |
Les deux sont fusionnées au handshake, et l’override gagne en cas de conflit de nom : une
classe peut ainsi remplacer une action héritée sans toucher au parent
(RealtimeController.ts:453). Un endpoint sans aucune action ne paie rien — la table du pair est
allouée au premier enregistrement seulement.
Ce que ton return et ton throw deviennent sur le fil#
Le retour du handler est envoyé tel quel en result. Les erreurs, elles, suivent une règle Zero
Trust stricte, appliquée dans JsonRpcPeer.handleRequest() (JsonRpcPeer.ts:506) :
| Côté serveur | Ce que reçoit le client | Pourquoi |
|---|---|---|
return valeur |
result: valeur |
le contrat nominal |
throw new RpcError(msg, code) |
error avec ton code et ton message |
tu as choisi d’exposer ce refus |
throw de toute autre erreur |
-32603 "internal error", opaque |
un message d’exception fuite chemins et schémas |
| action non enregistrée | -32601 "method not found: <nom>" |
découverte honnête d’un nom inconnu |
| frame refusée par le verrou | -32001 "unauthorized" |
motif générique : pas d’oracle d’autorisation |
⚠️ Attention
Le temps réel n’a qu’UNE erreur, et elle vient du cœur. Pour choisir ce que voit le client, c’est
RpcError(JsonRpcPeer.ts:71), importée depuisnodefony— soncodeest un entier JSON-RPC et sondatatraverse le fil. Toute autre exception donne un-32603opaque. Ne pas chercher d’erreur maison dans@nodefony/realtime: il n’y en a pas, ettests/unit/errorSurface.test.tsrefuse d’en exporter une que personne ne lève.
🔐 Autorisation — qui peut appeler quoi#
❗ Important
La découverte n’est pas une protection. Le champ
methodsde l’accueil sert à construire une interface honnête, pas à garder une porte. Un client peut forger une frame pour n’importe quel nom, annoncé ou non : la seule défense est côté serveur.
Cette défense est le verrou de frame, posé par @nodefony/security. Il examine chaque frame
entrante et, pour une action, résout une politique par son nom — exactement le mécanisme des
canaux (buildFrameAuthorizer(), frameAuthorizer.ts:388 ; branche des méthodes,
frameAuthorizer.ts:388). Trois situations, à connaître dans cet ordre :
Situation 1 — une action est FERMÉE par défaut#
@RealtimeAction("orders:quote") exige une connexion authentifiée, sans que tu aies rien à
écrire (realtimeDecorators.ts). Le décorateur pose pour toi la politique { authenticated: true }.
Le raisonnement : une action est une méthode que le pair appelle et qui agit — le défaut sûr
est de la fermer. Un visiteur anonyme se voit refuser la frame, et reçoit
realtime:denied { channel, reason: "forbidden" }.
Le décorateur accepte une politique en second argument, comme @RealtimeChannel et
@RealtimeInbound :
@RealtimeAction("orders:quote") // authentifié (défaut)
quote() { … }
@RealtimeAction("orders:purge", { roles: ["ROLE_ADMIN"] }) // rôle exigé
purge() { … }
@RealtimeAction("catalog:browse", { authenticated: false }) // PUBLIQUE, assumée
browse() { … }
Ouvrir une action reste donc possible — mais c’est désormais un acte délibéré et lisible à la déclaration, au lieu d’un silence. Si une politique est déjà déclarée sous le même nom (canal homonyme), elle est conservée : le défaut ne rétrograde jamais une règle plus stricte.
Situation 2 — les namespaces réservés portent un plancher#
Un nom qui commence par nodefony: hérite d’un plancher non contournable : authentifié et
ROLE_ADMIN (SYSTEM_CHANNEL_POLICY, frameAuthorizer.ts:70). Un seul préfixe couvre donc tout
ce que la plateforme expose (DEFAULT_SYSTEM_PREFIXES, frameAuthorizer.ts:86). C’est ce qui
protège nodefony:kernel:gc : le nom est la garde, et la comparaison est insensible à la casse
pour qu’un NODEFONY:kernel:gc ne passe pas à côté.
Ce plancher prime sur ta déclaration — y compris sur { authenticated: false }. Et il ne
s’arrête pas aux préfixes : tout nom contenant :health ou :stats est traité comme un canal
d’observabilité (matchSystemPolicy, frameAuthorizer.ts:202). Une action public:health reste
donc réservée, quoi qu’en dise son décorateur. À savoir avant de nommer une action « ouverte ».
Situation 3 — couvrir SES actions par la configuration#
Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
configuration de sécurité — la même liste que pour les canaux
(realtimeChannels, security/nodefony/config/config.ts:1093) :
use("@nodefony/security", {
realtimeChannels: [
// Couvre AUSSI bien le canal `orders:feed` que l'action `orders:quote` :
// le verrou résout une politique par NOM, sans savoir ce qu'il garde.
{
prefix: "orders:",
policy: { authenticated: true, roles: ["ROLE_USER"] },
},
],
});
La règle DEV-only#
Une action réservée au développement se contrôle dans le handler, côté serveur — jamais par un
drapeau d’interface. Cacher un bouton n’empêche personne de forger la frame. Le modèle est
nodefony:scaffold:run dans Studio, qui refuse net hors développement avant même de lire ses paramètres
(StudioRealtimeController.ts:133).
🛑 Prudence
Le défaut fermé protège de l’oubli, pas de la sur-ouverture. Une action mutable reste la surface la plus facile à sous-estimer : elle n’apparaît dans aucune table de routes HTTP et aucun test d’API REST ne la couvre. « Authentifié » ne veut pas dire « autorisé à faire ça » — pour une action qui écrit, exige un rôle. Avant de livrer, relis tes
@RealtimeActionet demande-toi, pour chacune : « qu’arrive-t-il si le premier compte venu l’appelle en boucle ? »
⏱️ Délai, abandon et rejeu#
Le délai d’expiration est la seule libération automatique#
request() prend le délai en troisième argument positionnel, en millisecondes — il n’y a pas
d’objet d’options (RealtimeClient.request(), RealtimeClient.ts:727) :
await socket.request("orders:quote", { orderId }); // 30 000 ms par défaut
await socket.request("orders:export", { scope }, 120_000); // action longue
| Défaut | Valeur | Ancrage |
|---|---|---|
request |
30 000 ms | JsonRpcPeer.ts:310 |
À l’expiration, l’entrée en attente est retirée et la Promise rejetée avec
RPC timeout: <méthode> (JsonRpcPeer.ts:455). Conséquence à connaître : une réponse qui
arriverait après coup ne trouve plus personne et est ignorée en silence
(JsonRpcPeer.ts:539) — pas de résolution tardive, pas de fuite.
Abandonner : ce qui existe vraiment#
🛑 Prudence
Il n’y a pas d’annulation d’un appel en cours. La socket n’expose ni
AbortControllernisignal: cherche-les, tu ne les trouveras pas. Et même s’ils existaient, ils ne changeraient rien au fait capital — abandonner l’attente n’arrête pas l’exécution serveur. Si le handler a commencé à modifier l’état, il finira son travail, seul.
Trois leviers existent, et ils ne font pas la même chose :
- Le délai d’expiration — libère le client, laisse le serveur travailler.
- La fermeture de la connexion —
dispose()rejette tous les appels en attente d’un coup (JsonRpcPeer.ts:453), appelé au nettoyage de la socket (RealtimeController.ts:553). Là encore : côté client seulement. - Une action compagnon — la seule vraie annulation. On expose une seconde action qui prend
l’identifiant du travail et l’interrompt côté serveur. Le modèle du dépôt est
nodefony:scaffold:cancel(StudioRealtimeController.ts:159), pendant denodefony:scaffold:run.
Rejouer : idempotence par action#
Un rejeu n’est pas un cas rare : une socket se reconnecte, une frame peut repartir, un utilisateur reclique. La question à se poser pour chaque action est donc « deux fois font-elles comme une fois ? ».
| Action | Idempotente ? | Rejeu sûr ? |
|---|---|---|
nodefony:kernel:ping |
oui — lecture pure | oui |
nodefony:kernel:gc |
oui — l’effet est un cycle GC | oui, mais coûteux (pause du process) |
api.request en lecture |
oui — c’est un GET |
oui |
nodefony:scaffold:cancel |
oui — annuler deux fois annule | oui |
nodefony:scaffold:run |
non — crée un travail | non : deux appels, deux jobs |
une mutation (socket.mutate) |
non par nature | oui, avec une clé d’idempotence |
Pour les mutations passant par le pont API, la clé n’est pas une convention : elle est exigée
par la signature de mutate() (RealtimeClient.ts:797), et c’est la garde @Idempotent
(routerDecorators.ts:1103) qui, côté serveur, reconnaît le rejeu et rend la réponse déjà calculée
au lieu de refaire l’effet.
await socket.mutate("/nodefony/security/api/apikeys/42/revoke", {
method: "POST",
idempotencyKey: crypto.randomUUID(), // rejouer cette frame ne révoque qu'une fois
});
❗ Important
Pour une action maison non idempotente, la même discipline s’applique mais rien ne l’impose : c’est à ton handler d’accepter un identifiant fourni par l’appelant et de mémoriser le résultat déjà rendu. Sans cela, une reconnexion malheureuse double l’effet — deuxième commande, deuxième courriel, deuxième débit.
🧩 Un flux ne tient pas dans une réponse#
C’est l’erreur de conception la plus fréquente : vouloir faire progresser un travail long à l’intérieur d’un appel. Une action, c’est un aller-retour — une seule réponse, envoyée à la fin. Une génération de code, un import de fichier, une réponse de modèle de langage produisent au contraire des résultats au fil de l’eau, pendant des minutes.
Le remède tient en trois gestes, et il est déjà en production dans Studio :
- L’action lance et rend un identifiant, sans attendre —
nodefony:scaffold:run(StudioRealtimeController.ts:133) démarre le travail et rend son état, dont sonid. - Le flux se diffuse sur un canal dédié, nommé d’après ce travail —
nodefony:scaffold:job@<id>, servi par le contrôleur (StudioRealtimeController.ts:190). - Le client s’abonne à ce canal et regarde le travail se faire, ligne après ligne
(
Create.tsx:323).
La course, et comment le dépôt la neutralise#
Le danger classique de ce découpage est une course : entre le retour de l’action et l’abonnement du client, les premières lignes sont émises et personne ne les écoute. La règle prudente est donc de s’abonner avant de lancer l’appel.
Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
canal rejoue son historique au nouvel abonné, de sorte qu’un arrivant tardif voit tout depuis
le début (ScaffoldService.subscribe(), ScaffoldService.ts:425). C’est ce qui autorise le front
à faire l’appel d’abord et à s’abonner ensuite, sans rien perdre.
💡 Astuce
Retiens la règle sous cette forme : s’abonner avant de lancer, sauf si le producteur du canal rejoue explicitement son historique. Un canal sans historique ne rattrape rien — c’est une propriété du module : un abonnement ne donne accès qu’à l’avenir.
Et le streaming du protocole ?#
Il n’y en a pas. Une action rend une valeur (RpcActionHandler, JsonRpcPeer.ts:118), que
le pair emballe en une réponse unique. Pour tout ce qui progresse — une réponse mot à mot d’un
modèle de langage, un export qui avance, un traitement long — la voie est le motif
« travail + canal » : l’action accuse réception, et la progression arrive sur un canal.
Le protocole a porté un temps une réception en morceaux dont aucun serveur n’émettait la contrepartie : elle a été retirée plutôt que publiée à moitié. Un vrai streaming demande de pouvoir annuler un flux en cours, signaler une erreur au milieu et réguler le débit — il sera conçu avec son premier consommateur réel.
🔌 Le pont API — la même action, servie sur la socket#
Un cas particulier mérite d’être connu avant d’écrire une action : elle existe peut-être déjà en
HTTP. Le pont API expose la méthode api.request, qui rejoue une route de contrôleur sur la
socket, avec la même garde et le même résultat qu’en REST — invokeApiRequest()
(RealtimeController.ts:818). Il est désactivé par défaut et s’active en surchargeant
realtimeApiRequest() (RealtimeController.ts:219).
const modules = await socket.request("/nodefony/kernel/api/modules");
La forme se discrimine toute seule : un chemin commence par /, jamais un nom d’action —
RealtimeClient.request() (RealtimeClient.ts:727). Écris une action RPC pour ce qui n’a de sens que sur la socket ;
passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit dans le
vocabulaire et l’architecture.
⚠️ Pièges#
| Symptôme | Cause | Correction |
|---|---|---|
RPC timeout: <méthode> alors que le serveur a bien répondu |
le handler dure plus que 30 s, le défaut | passer le délai en 3ᵉ argument : request(m, p, 120_000) |
| 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 |
-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 |
-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 |
-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_ADMIN |
| Une action sensible est appelable par un anonyme | une action applicative est libre tant qu’aucune politique ne la couvre | ajouter une règle de préfixe (security/nodefony/config/config.ts:906) |
| 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 |
abort() introuvable sur la socket |
il n’existe pas — et n’arrêterait pas le serveur de toute façon | exposer une action d’annulation qui prend l’identifiant du travail |
| Les premières lignes d’un job manquent | abonnement au canal après le début de la production, sans historique rejoué | s’abonner avant de lancer, ou faire rejouer l’historique par le producteur |
Toutes les Promise rejettent d’un coup |
la connexion s’est fermée : dispose() vide la table des appels en attente |
attendu ; relancer après reconnexion, l’appel n’a pas survécu |
🧪 Tests & couverture#
Les actions sont couvertes à trois étages — protocole, contrôleur, sécurité. Les chiffres exacts vivent dans la carte de l’aperçu, régénérée depuis les résultats réels, jamais figés ici.
- Unitaires, protocole (
JsonRpcPeer.test.ts) : corrélation requête/réponse, méthode inconnue-32601,throwopaque-32603, propagation fidèle d’uneRpcError, expiration du délai, audit des frames. - Unitaires, déclaration (
realtimeDecorators.test.ts) :@RealtimeActionenregistre bien le nom et lie lethis; (RealtimeController.test.ts) : fusion décorateurs + override et annonce dans l’accueil. - Unitaires, client (
RealtimeClientCoverage.test.ts) : formes derequest, contrat de streaming, rejet des appels en attente audispose. - Attaque, autorisation (
realtimeFrameLock.test.ts) :nodefony:kernel:pingetnodefony:kernel:gcrefusés à l’anonyme et à l’utilisateur authentifié, acceptés à l’administrateur — la preuve que le plancher de namespace garde bien les actions, pas seulement les canaux. - Bout en bout (
realtimeControllerPaths.e2e.test.ts,realtimeChannelAuth.e2e.test.ts) : serveur réel, résolution d’endpoint et pont API. - Ce qui manque : aucun banc de charge dédié aux actions (le débit mesuré est celui du
fan-out, pas celui des appels corrélés). Pour en monter un, voir le skill
nodefony-load-test; pour la mémoire,nodefony-check-memory-health.
Couverture : npm run coverage dans @nodefony/realtime.
🔗 Pour aller plus loin#
-
⬆️ Retour au hub : Realtime — vue d’ensemble · Toute la documentation
-
🧭 Pages sœurs : Vocabulaire · Architecture · Sécurité · Configuration · Cookbook — un chat complet
-
Les mots employés ici, avec leurs analogies → vocabulaire
-
Le trajet d’une frame, du transport au hub → architecture
-
Le verrou de frame, le handshake, les politiques de canal → sécurité
-
La garde qui décide, côté HTTP comme WebSocket → firewall
-
Les signatures exactes ne sont pas recopiées ici : elles vivent dans le graphe symbolique
.ai/symbols.json, régénéré depuis les TSDoc du code.