Le gradient
Le gradient permet de faire passer une Frond d'une exécution locale à un processus distant sans modifier son code métier ni les pages qui l'appellent.
L'énoncé de topologie
// fougere.config.ts
export default defineFougere({
remotes: { blog: 'http://127.0.0.1:4100' },
});
Une Frond déclarée dans remotes reste scannée : ses entités fournissent toujours les
métadonnées utilisées par les formulaires, la validation et la DI. Ses opérations sont en
revanche exécutées à l'adresse distante. Retirer cette ligne rétablit l'exécution locale.
La démo multi-Frond couvre les deux configurations, y compris en build de production :
pnpm dev:blog # la Frond blog seule, dans son process (:4100)
pnpm dev # l'app — la consomme via la ligne remotes
Le contrat d'appel
Un appel est une valeur : (entity, operation, invocation) avec
invocation = { params, query, body, state }.
createLocalRunnerexécute strictement en local ;createAppRunnersuit la topologie — façades locales, doublures distantes ;- les transports sérialisent cette valeur sans changer sa structure.
Le format du fil
Process-à-process, c'est JSON-RPC 2.0 sur POST /_fougere/call :
// → requête
{ "jsonrpc": "2.0", "id": 1, "method": "post.publish",
"params": { "params": { "id": "…" }, "query": {}, "body": null, "state": { "user": { … } } } }
// ← succès
{ "jsonrpc": "2.0", "id": 1, "result": { "id": "…", "status": "published", … } }
// ← échec métier — ravivé en FougereError côté appelant
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32000,
"data": { "code": "CONFLICT", "message": "Déjà publié",
"entity": "post", "operation": "publish" } } }
method est entity.op et params contient l'invocation. Le navigateur envoie la même
trame à Nitro, mais son state est ignoré et reconstruit côté serveur.
L'autre moitié du contrat est ce qu'un hôte répond à rpc.discover : voir
La carte d'identité, qui spécifie le document et ce qu'il faut
honorer pour écrire une Frond dans un autre langage.
Le saut est en loopback par défaut
Le récepteur écoute 127.0.0.1 et plafonne les corps à 1 Mio. L'élargir est une option :
await serve(runner, { hosts: ['0.0.0.0'] }) // un conteneur, qui le dit
Le défaut n'est pas de la prudence, et l'option n'est pas un trou : c'est l'endroit où un
fait rencontre un déploiement. Un récepteur lit l'identité de l'appelant sur le fil et ne
la rétablit pas, donc quiconque atteint le port peut se dire n'importe quel utilisateur.
En loopback, cet ensemble vaut « cette machine ». Élargissez hosts et il devient ce que
le réseau laisse passer — à l'exploitant de le refermer : pare-feu, sidecar, mesh.
Authentifier le lien ne le remplacerait pas non plus : un secret partagé dit qu'un process a le droit d'appeler, jamais au nom de quel utilisateur il parle. L'identité au niveau de la Frond est la question ouverte.
Comportement après séparation
| Élément | Comportement |
|---|---|
| résultats | formes identiques des deux côtés, listes comprises |
| erreurs | même FougereError avec code, message et details, reconstruite à l'arrivée |
| state | la session de l'app consommatrice atteint les collectors distants |
| hôte inaccessible | SERVICE_UNAVAILABLE typée → statut 503 ; les appels reprennent après le redémarrage de l'hôte |
| transport | timeout et retry sur l'enveloppe, sans retry automatique sur une command |
Le prix du split
Le split ajoute un saut HTTP et l'encodage/décodage JSON qu'il implique. Un appel reste
une valeur (entity, operation, invocation) ; le transport la met sur le fil et l'en
retire, puis le côté receveur exécute la même façade que le chemin local. Le coût dépend
donc du réseau, de la taille de la charge et du runtime : Fougere ne prétend pas le rendre
nul.
Aucun chiffre n'est cité ici : ce dépôt ne livre pas de harnais de bench, et un nombre qu'on ne peut pas rejouer n'est pas une mesure.
Où le code vit
Une Frond peut aussi être déplacée dans un dépôt distinct et rester appelée via remotes.
Trois choses s'appellent « ensemble » et une seule est obligatoire : le contrat doit
voyager, le code non, les ports doivent se joindre.
Deux commandes couvrent le contrat, selon ce que vous voulez partager :
| Ce qu'elle fait | Quand | |
|---|---|---|
fougere sync | demande rpc.discover à l'hôte et reconstruit ses entités en local | l'hôte tourne ; c'est aussi la seule voie pour une Frond écrite dans un autre langage |
fougere build-frond | compile entities/** en paquet installable | vous publiez le contrat comme une dépendance |
Le code source de la Frond ne traverse dans aucun des deux cas. Ce qui reste ouvert, ce sont les ports : voir Déploiement.
Un destinataire, et un seul
remotes nomme une adresse par Frond, et Facade<T> résout exactement une façade —
facadeKeyOf produit une clé, le conteneur rend un objet. Tout appel dans Fougere a un
destinataire unique, par construction.
Ça couvre plus large qu'il n'y paraît. Un appareil derrière un NAT, qu'on ne peut pas
appeler, déclare quand même remotes et envoie vers une passerelle qu'il nomme : un
destinataire — et le fait qu'il doive ouvrir la connexion lui-même est une propriété de
déploiement, pas un autre genre d'appel.
Ce qui n'est pas couvert, c'est plusieurs destinataires pour un envoi :
| qui détermine les destinataires | |
|---|---|
| une flotte | l'émetteur nomme l'ensemble |
| un fait — un post a été publié | personne : ceux qui ont déclaré s'y intéresser |
Les deux demandent la même machinerie — le fan-out, un canal vers des membres qui ne portent pas d'adresse, et un verdict quand trois sur cinq réussissent. Fougere n'a rien de tout ça. Donc « un post est publié, la recherche réindexe et la newsletter met en file » s'écrit aujourd'hui en nommant les deux à l'endroit qui émet : l'émetteur porte la liste de tout ce qui découle de son propre geste, et un troisième lecteur le rouvre.
Cette section existe pour nommer la moitié qui est là. remotes se lit comme *l'*énoncé de
topologie et il en est un sur deux ; l'autre est conçu et non livré. Le dire garde le
manque visible plutôt que surprenant.
Limites
Le passage à une Frond distante ajoute les pannes et la latence propres au réseau. Fougere
les expose sous forme d'erreurs typées, mais ne les masque pas. Le mode local reste le mode
de référence ; remotes permet de changer la topologie lorsque le besoin apparaît.
Suite : Surfaces — les mêmes opérations derrière REST et GraphQL.