Wrapper de Timeout pour Opérations NDK
Problème
Les opérations NDK (nostr-dev-kit) comme fetchEvents() et ndkEvent.publish() n'ont pas de timeout intégré. Quand les connexions aux relais s'immobilisent ou deviennent non-réactives, ces opérations s'accrochent indéfiniment, causant le gel de l'application sans message d'erreur.
Contexte / Conditions Déclenchantes
- L'application se fige lors d'opérations Nostr
- Aucune erreur de timeout levée malgré des minutes d'attente
- Fonctionne parfois, s'accroche aléatoirement (dépendant du relai)
- Le log montre l'opération démarrée mais jamais complétée
- Utilisation de NDK avec plusieurs relais dont certains peuvent être peu fiables
Solution
Créez une fonction wrapper de timeout :
const NDK_TIMEOUT_MS = 30000; // 30 secondes
async function withTimeout<T>(
promise: Promise<T>,
ms: number,
operation: string
): Promise<T> {
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error(`${operation} timed out after ${ms}ms`)),
ms
);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clearTimeout(timeoutId!);
return result;
} catch (error) {
clearTimeout(timeoutId!);
throw error;
}
}
Enrobez toutes les opérations NDK :
// Connexion avec timeout
await withTimeout(ndk.connect(), NDK_TIMEOUT_MS, "NDK connect");
// Récupérer les événements avec timeout
const events = await withTimeout(
ndk.fetchEvents({ kinds: [0], authors: [pubkey] }),
NDK_TIMEOUT_MS,
"fetch profile"
);
// Publier avec timeout
const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk);
await withTimeout(
ndkEvent.publish(relaySet),
NDK_TIMEOUT_MS,
"relay publish"
);
Assurez-vous aussi que les erreurs de timeout sont retriables :
function isRetryableError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase();
const errorName = error.name.toLowerCase();
if (
message.includes("timeout") ||
message.includes("aborted") ||
errorName.includes("timeout") ||
errorName.includes("abort")
) {
return true;
}
}
return false;
}
Vérification
Après implémentation, les opérations qui s'accrochaient auparavant doivent maintenant :
- Lever une erreur de timeout après la durée spécifiée
- Permettre à la logique de retry de réessayer l'opération
- Logger l'opération spécifique qui a expiré
Exemple
Avant (s'accroche indéfiniment) :
const events = await ndk.fetchEvents({ kinds: [34236], "#d": [vineId] });
Après (expire et peut retrier) :
const events = await withTimeout(
ndk.fetchEvents({ kinds: [34236], "#d": [vineId] }),
30000,
"check video exists"
);
Notes
- 30 secondes est une durée par défaut raisonnable ; ajustez selon la durée d'opération attendue
- Envisagez des timeouts plus courts pour les vérifications d'existence, plus longs pour les opérations par lot
- Enrobez TOUTES les opérations NDK, pas seulement celles problématiques (n'importe laquelle peut s'accrocher)
- Ce pattern s'applique à toute bibliothèque asynchrone sans timeouts intégrés
- Pour le support d'AbortController (si la bibliothèque le supporte), préférez-le à Promise.race
Références
- NDK GitHub : https://github.com/nostr-dev-kit/ndk
- Pattern Promise.race : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race