# Ajouter un nœud API avec retries à Workify

Ajoutez une factory qui retourne un `INode` et encapsule l’appel HTTP dans `execute()`. Le moteur existant n’a pas besoin d’être modifié : il attend la promesse du nœud, transporte sa sortie et applique déjà les règles d’erreur et d’annulation du workflow.

Les fichiers `api-node.ts` et `usage.ts` sont un exemple d’extension, distinct du paquet core livré. Ils importent seulement les types Workify dans le module du nœud, utilisent les API standard `fetch`, `AbortController` et les timers, et ne nécessitent aucune dépendance d’exécution supplémentaire. Remplacez `api.example.com` par votre API ; aucun appel réseau réel n’a été effectué pour préparer cet exemple.

## Enregistrement dans le workflow

```ts
import { Engine, Workflow } from '@newvexa/workify';
import { createApiNode } from './api-node.js';

const workflow = new Workflow({ id: 'users', name: 'Utilisateurs' });
workflow.addNode(createApiNode({
  id: 'fetch-users',
  name: 'Charger les utilisateurs',
  url: () => 'https://api.example.com/users',
  method: 'GET',
  maxAttempts: 4,
  baseDelayMs: 300,
  maxDelayMs: 5000,
  requestTimeoutMs: 10000,
  onRetry: event => console.log(event.nextAttempt, event.delayMs),
}));

const engine = new Engine(workflow);
const controller = new AbortController();
const result = await engine.run(null, { signal: controller.signal });
console.log(result.status, result.results['fetch-users']?.outputs.main);
```

`maxAttempts: 4` signifie **quatre tentatives au total**, pas quatre retries. La sortie `main` contient le JSON de la réponse. `usage.ts` montre également une URL construite à partir du payload et un lien vers un deuxième nœud. Le domaine reste fixe afin d’éviter qu’un payload non fiable ne choisisse librement un hôte, notamment côté serveur.

## Déroulement de `execute()`

Le nœud construit son URL, ses headers et son corps une seule fois pour chaque exécution. Chaque tentative utilise un nouveau contrôleur d’annulation, lié au signal du moteur et à un timeout. Le timeout couvre `fetch()` **et la lecture du corps**, pas seulement la réception des headers [1]. Les timers et listeners ajoutés par l’extension sont retirés à la fin de chaque tentative.

En cas d’échec transitoire, le nœud attend un délai exponentiel avec **full jitter** : un délai aléatoire entre zéro et le plafond exponentiel, plafonné par `maxDelayMs`. Cela évite de synchroniser systématiquement les nouveaux essais de plusieurs clients. L’attente est elle aussi annulable via `context.signal`.

La politique de cet exemple réessaie les statuts **408, 429, 500, 502, 503 et 504**, les timeouts et les erreurs `TypeError` provenant de fetch. Cette liste est un choix de l’extension, pas une règle imposée par le core : adaptez-la au contrat de votre fournisseur. Les autres statuts, les réponses JSON mal formées et les erreurs de validation ne sont pas réessayés.

Le header `Retry-After` peut contenir un nombre entier de secondes ou une date HTTP [2]. L’extension attend au moins ce délai. Si le serveur exige un délai supérieur à `maxDelayMs`, elle **échoue sans réessayer**, plutôt que de tronquer le délai et rappeler le serveur trop tôt. Une réponse vide réussie, notamment 204, produit `null`.

## Idempotence : ne pas dupliquer les opérations

GET, PUT et DELETE sont idempotents selon leur sémantique HTTP ; POST et PATCH ne sont pas garantis idempotents [3]. Dans l’exemple, POST/PATCH ne sont donc jamais réessayés par défaut. Une erreur réseau peut survenir **après** que le serveur a appliqué une opération : réessayer aveuglément un paiement ou une création peut la dupliquer.

Si votre fournisseur garantit l’idempotence, vous pouvez activer explicitement les retries pour cette requête :

```ts
const requestKey = 'identifiant-stable-de-l-operation';
const paymentNode = createApiNode({
  id: 'create-operation',
  name: 'Créer une opération',
  url: () => 'https://api.example.com/operations',
  method: 'POST',
  idempotent: true, // Seulement si le serveur garantit ce contrat.
  headers: { 'Idempotency-Key': requestKey },
  body: () => ({ amount: 1000, currency: 'EUR' }),
  maxAttempts: 3,
});
```

Le header seul ne crée aucune garantie : le serveur doit le prendre en charge. La clé et le corps restent identiques durant les retries d’une même opération. Utilisez une nouvelle clé pour une nouvelle opération ; réutilisez celle de l’opération initiale si vous en reprenez l’exécution, selon le contrat du fournisseur. La chaîne ci-dessus est un placeholder, pas une clé à partager entre toutes les opérations. `idempotent: false` désactive les retries même pour un GET.

## État et observabilité

Pendant les essais et les attentes, le nœud conserve le statut `running`. S’il finit par réussir, il devient `succeeded`. Une fois les tentatives épuisées, l’exception remonte au moteur : le nœud devient `failed`, ses descendants sont `blocked` et les branches indépendantes continuent. Une annulation du workflow n’est jamais transformée en retry.

`onRetry` expose le numéro de tentative, le délai, le `runId` et le `nodeId` à votre interface ou système de logs. Ce callback appartient à l’extension : il n’ajoute **aucun nouveau type d’événement** au core et les retries intermédiaires n’entrent pas dans son journal de statuts. Les exceptions synchrones du callback sont isolées ; ne fournissez pas un callback asynchrone sans gérer ses rejets.

Dans la v0, le créneau de concurrence du nœud reste occupé pendant son backoff. C’est sûr et simple, mais réduit le débit lorsque beaucoup de nœuds attendent. Pour de grandes charges, un planificateur conscient des retries pourrait remettre les nœuds en file avec une date de disponibilité et libérer le créneau pendant l’attente. Cette optimisation demanderait une extension des états et du moteur ; elle n’est pas ajoutée implicitement ici.

## Précautions navigateur et serveur

Dans le navigateur, respectez CORS et configurez `Access-Control-Expose-Headers: Retry-After` côté API si vous devez lire ce header en cross-origin [4]. Ne mettez jamais un secret serveur dans un bundle front-end ; utilisez votre backend pour les appels privilégiés.

Une `TypeError` de fetch ne permet pas de distinguer proprement une coupure réseau d’une erreur CORS ou d’une URL invalide. Le nombre d’essais reste borné, mais les retries ne corrigent pas une mauvaise configuration. Les délais sont locaux, sans persistance après redémarrage ; le jitter ne remplace pas un rate limiter partagé.

Validez le schéma métier de la réponse dans votre nœud suivant. L’exemple garantit seulement un payload JSON compatible avec le moteur. Ne journalisez pas les tokens, headers d’autorisation ou corps sensibles dans `onRetry`. Un timeout ou une annulation ne garantit pas que le serveur ait annulé une opération déjà reçue.

## Utilisation des fichiers

Copiez `api-node.ts` et `usage.ts` dans votre projet consommateur du paquet Workify. La compilation cible ES2022 avec `lib: ["ES2022", "DOM"]`, comme le core. Les tests associés utilisent des fetch simulés et n’appellent aucun service réel.

L’extension et son exemple d’utilisation ont passé la compilation TypeScript stricte, sans `any` ni `as any`. Les **19 tests unitaires ont réussi**, couvrant les statuts HTTP, les méthodes non idempotentes, les corps stables, les timeouts, l’annulation, `Retry-After` et le parsing JSON. Depuis la racine du dépôt, lancez `npm run test:http` : le script génère temporairement `api-node.mjs`, exécute les tests puis supprime le module généré. Seules les sources et les tests sont versionnés.

## Références

[1]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal "MDN — AbortSignal et annulation de fetch"
[2]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After "MDN — formats du header Retry-After"
[3]: https://developer.mozilla.org/en-US/docs/Glossary/Idempotent "MDN — idempotence des méthodes HTTP"
[4]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers "MDN — exposition des headers aux requêtes cross-origin"
