ndk-operation-timeout-wrapper

Par divinevideo · divine-mobile

Résoudre le blocage indéfini des opérations NDK (Nostr Dev Kit) lorsque les connexions aux relais se figent. À utiliser quand : (1) L'application se bloque lors d'appels `fetchEvents` ou `publish`, (2) Aucune erreur de timeout malgré des problèmes réseau, (3) La connexion au relais semble bloquée, (4) Utilisation de NDK avec des relais instables ou lents. Les opérations NDK n'ont pas de timeout intégré — encapsulez-les avec `Promise.race`.

npx skills add https://github.com/divinevideo/divine-mobile --skill ndk-operation-timeout-wrapper

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 :

  1. Lever une erreur de timeout après la durée spécifiée
  2. Permettre à la logique de retry de réessayer l'opération
  3. 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

Skills similaires