PD-284 — Agent Developer : Module seal-sse-client¶
Livrable : src/seal/sse-client.ts¶
Résumé¶
Ce module implémente le client SSE (Server-Sent Events) fetch-based pour le suivi temps réel des scellements PD-284. Il couvre :
- Connexion SSE via
fetch+ReadableStream(parsing manueltext/event-stream) - Reconnexion avec backoff exponentiel :
delay = base × factor^attempt(1s, 2s, 4s par défaut) - Plafond cumulé
max_cumulative_delay=30savant failover immédiat - Failover polling à 5s après épuisement des tentatives SSE
- Tentative SSE parallèle à chaque cycle de polling (reprise automatique)
- Arrêt polling immédiat dès reconnexion SSE
- INV-284-09 : aucun badge
Hors lignedéclenché par ce module (responsabilité NetInfo dans C9)
Décisions architecturales¶
architectural_decisions:
- decision: "Factory function `createSSEClient()` au lieu d'une classe"
rationale: "Encapsulation par closures — l'état interne (timers, AbortController) est inaccessible depuis l'extérieur. Plus idiomatique en React/hooks."
alternatives_considered:
- "Classe SSEClient — expose l'état interne via properties, plus complexe à tester avec mocks"
- "Hook React `useSSE()` — couple le transport au lifecycle React, empêche l'utilisation depuis l'orchestrateur C7"
trade_offs:
- "Factory ne supporte pas le pattern `instanceof` — non nécessaire ici"
- "Les closures capturent les références : attention au GC si le client n'est pas disconnect()"
Code source¶
Le fichier src/seal/sse-client.ts a été créé avec l'implémentation complète. Voici les éléments structurants :
Interfaces exportées¶
// Configuration du client SSE
export interface SSEClientConfig {
readonly baseUrl: string;
readonly getAuthToken: () => Promise<string>;
readonly sealConfig?: Partial<SealConfig>;
readonly buildPollingUrl: (sealId: SealId) => string;
readonly buildSseUrl: (sealId: SealId) => string;
}
// Interface publique du client
export interface SSEClient {
connect(sealId: SealId): void;
disconnect(): void;
readonly transportMode: TransportMode;
}
export type TransportMode = "SSE" | "POLLING" | "DISCONNECTED";
export type SSEEventCallback = (data: unknown) => void;
export type SSETransportChangeCallback = (mode: TransportMode) => void;
Factory¶
export function createSSEClient(
config: SSEClientConfig,
onEvent: SSEEventCallback,
onTransportChange?: SSETransportChangeCallback,
): SSEClient;
Mécanismes clés¶
-
Parsing SSE manuel :
readSSEStream()décode le fluxtext/event-streamligne par ligne, accumule les lignesdata:, et émet un événement JSON à chaque ligne vide (fin de bloc SSE). Les lignes de commentaire (:) sont ignorées (heartbeats). -
Backoff exponentiel (
reconnectWithBackoff) : - Boucle de
0àmax_attempts - 1 delay = base × factor^attempt→ 1s, 2s, 4s avec les defaults- Accumulation de
cumulativeDelay— si> max_cumulative_delay→ failover immédiat sans attendre les tentatives restantes -
Chaque tentative crée un nouvel
AbortControllerpour annulation propre -
Failover polling (
startPolling) : setIntervalàpolling_interval_after_sse_failover × 1000ms- Chaque tick :
pollOnce()(GET status) +attemptSSEReconnectFromPolling()en parallèle -
Si SSE reconnecte →
stopPolling()immédiat +setTransport("SSE") -
Lifecycle :
connect()est idempotent — disconnect l'instance précédentedisconnect()abort toutes les connexions (AbortController), stop le polling, clear les timers de retry- Le flag
disconnectedest vérifié à chaque point de reprise async pour éviter les effets de bord post-disconnect
Matrice de couverture invariants¶
| Invariant contract | Mécanisme dans ce module | Vérifié par |
|---|---|---|
| Backoff exponentiel : delay = base × factor^attempt (base=1s, factor=2, max_attempts=3) | reconnectWithBackoff() — boucle avec baseDelay * Math.pow(factor, attempt) | Test unitaire : mesure des délais entre tentatives (TC-NOM-08) |
| Failover polling à 5s après max_attempts échecs SSE consécutifs | startPolling() appelé après échec de reconnectWithBackoff() | Test intégration : journal backoff→polling (TC-NOM-08) |
| Plafond cumulé max_cumulative_delay=30s avant failover immédiat | Guard cumulativeDelay > maxCumulative dans boucle backoff | Test unitaire : failover avant max_attempts si cumulé dépasse 30s |
| Tentative SSE parallèle à chaque cycle polling | attemptSSEReconnectFromPolling() appelé à chaque tick setInterval | Test intégration : SSE attempt observable à chaque cycle (TC-NOM-09) |
| INV-284-09 : Échec SSE sans perte réseau NE déclenche PAS le badge Hors ligne | Module ne modifie aucun état réseau/offline — responsabilité hook NetInfo dans C9 | Review code : aucune référence à navigator.onLine ou NetInfo |
| Arrêt polling immédiat dès que SSE reconnecte | stopPolling() dans attemptSSEReconnectFromPolling() dès response.ok | Test intégration : polling arrêté après reconnexion SSE (TC-NOM-09) |
Vérification des interdits (forbidden)¶
| Interdit | Conformité |
|---|---|
| Déclencher le badge Hors ligne sur échec SSE seul | Le module n'a aucune dépendance sur NetInfo ni aucun mécanisme de badge. Le transport change silencieusement de SSE à POLLING via le callback onTransportChange. |
| Utiliser un délai fixe entre tentatives de reconnexion | delay = baseDelay * Math.pow(factor, attempt) — exponentiel garanti. |
| Continuer le polling après reconnexion SSE réussie | stopPolling() appelé dans attemptSSEReconnectFromPolling() avant le switch transport SSE. |
| Bloquer l'UI pendant la reconnexion | Toutes les opérations sont async. connect() est non-bloquant (fire-and-forget via connectInternal()). Aucun await synchrone visible côté appelant. |
| Utiliser une lib EventSource tierce (sauf clause CE-SSE-01) | Implémentation fetch-based pure. Aucune dépendance externe. Si HT-03 invalidée, un wrapper conforme à SSEClient peut être substitué. |
Hypothèses¶
| ID | Hypothèse | Impact si faux | Mitigation dans le code |
|---|---|---|---|
| HT-03 | Expo SDK 54 supporte fetch streaming (response.body.getReader()) | SSE impossible nativement | La factory accepte n'importe quelle implémentation SSEClient — un wrapper react-native-sse peut être substitué (clause CE-SSE-01) |
| HT-08 | SSE heartbeat TTL = 30s | Reconnexion trop tardive si heartbeat plus fréquent | Les paramètres sont configurables via SealConfig |
Fichiers hors périmètre identifiés¶
Aucun. Le module seal-sse-client ne dépend que de src/types/seal.ts (C1) et src/seal/telemetry.ts (C13), tous deux dans le périmètre autorisé.
Tests contractuels couverts¶
| Test ID | Point d'observation dans ce module |
|---|---|
| TC-NOM-08 | reconnectWithBackoff() : backoff 1s/2s/4s → startPolling() → pas de badge offline |
| TC-NOM-09 | attemptSSEReconnectFromPolling() : SSE reconnecte → stopPolling() |
| TC-NOM-13 (proxy) | Le module émet les événements via onEvent() sans traitement bloquant — latence ajoutée ≈ 0 |
| TC-NR-03 | Régression failover : backoff → polling → retour SSE — séquence complète testable |
| TC-NEG-09 | Échec SSE sans perte réseau → transport passe à POLLING, pas de badge offline |