story-research-zapwall/docs/code-analysis-duplication-optimization.md
2026-01-07 03:10:40 +01:00

19 KiB

Analyse de duplication, mutualisation et optimisation du code

Date : 2024-12-19 Auteur : Équipe 4NK

Résumé exécutif

Cette analyse identifie les duplications de code, les opportunités de mutualisation/centralisation et les axes d'organisation et d'optimisation dans le codebase. L'objectif est de réduire la duplication, améliorer la maintenabilité et optimiser l'architecture sans dégrader les performances.

1. Duplications identifiées

1.1 Initialisation IndexedDB (Critique - Forte duplication)

Localisation : Multiple fichiers avec pattern identique

Fichiers concernés :

  • lib/notificationService.ts (lignes 39-89)
  • lib/publishLog.ts (lignes 24-72)
  • lib/storage/indexedDB.ts (lignes 25-72)
  • lib/objectCache.ts (lignes 33-86)
  • lib/localeStorage.ts (lignes 14-43)
  • lib/settingsCache.ts (lignes 24-54)
  • public/writeWorker.js (lignes 404-479)

Pattern dupliqué :

private async init(): Promise<void> {
  if (this.db) return
  if (this.initPromise) return this.initPromise
  this.initPromise = this.openDatabase()
  try {
    await this.initPromise
  } catch (error) {
    this.initPromise = null
    throw error
  }
}

private openDatabase(): Promise<void> {
  return new Promise((resolve, reject) => {
    if (typeof window === 'undefined' || !window.indexedDB) {
      reject(new Error('IndexedDB is not available'))
      return
    }
    const request = window.indexedDB.open(DB_NAME, DB_VERSION)
    request.onerror = () => reject(...)
    request.onsuccess = () => { this.db = request.result; resolve() }
    request.onupgradeneeded = (event) => { /* schema creation */ }
  })
}

Impact :

  • ~200 lignes de code dupliquées
  • Maintenance difficile (changements à appliquer en 7+ endroits)
  • Risque d'incohérences entre implémentations

Solution proposée : Créer un utilitaire générique lib/indexedDBHelper.ts avec factory pattern


1.2 Pattern de subscription avec relay rotation (Critique - Forte duplication)

Localisation : lib/userContentSync.ts

Fonctions concernées :

  • fetchAndCachePublications (lignes 22-146)
  • fetchAndCacheSeries (lignes 151-281)
  • fetchAndCachePurchases (lignes 286-383)
  • fetchAndCacheSponsoring (lignes 388-485)
  • fetchAndCacheReviewTips (lignes 490-587)
  • fetchAndCachePaymentNotes (lignes 599-721)

Pattern dupliqué (~50 lignes par fonction) :

// 1. Récupération lastSyncDate
const { getLastSyncDate } = await import('./syncStorage')
const lastSyncDate = await getLastSyncDate()

// 2. Construction des filters
const filters = [{ ...buildTagFilter(...), since: lastSyncDate, limit: 1000 }]

// 3. Tentative avec relay rotation
const { createSubscription } = require('@/types/nostr-tools-extended')
let sub = null
let usedRelayUrl = ''
try {
  const result = await tryWithRelayRotation(
    pool as unknown as import('nostr-tools').SimplePool,
    async (relayUrl, poolWithSub) => {
      usedRelayUrl = relayUrl
      // Notification syncProgressManager
      const { syncProgressManager } = await import('./syncProgressManager')
      const currentProgress = syncProgressManager.getProgress()
      if (currentProgress) {
        syncProgressManager.setProgress({
          ...currentProgress,
          currentStep: 0,
          currentRelay: relayUrl,
        })
      }
      return createSubscription(poolWithSub, [relayUrl], filters)
    },
    5000
  )
  sub = result
} catch {
  // Fallback to primary relay
  usedRelayUrl = getPrimaryRelaySync()
  sub = createSubscription(pool, [usedRelayUrl], filters)
}

// 4. Gestion des événements avec Promise + timeout
const events: Event[] = []
return new Promise<void>((resolve) => {
  let finished = false
  const done = async () => { /* ... */ }
  sub.on('event', (event) => { events.push(event) })
  sub.on('eose', () => void done())
  setTimeout(() => void done(), 10000).unref?.()
})

Impact :

  • ~300 lignes de code dupliquées
  • Logique de gestion d'événements répétée 6 fois
  • Risque d'incohérences dans la gestion des erreurs et timeouts

Solution proposée : Créer une fonction générique createSyncSubscription dans lib/syncSubscriptionHelper.ts


1.3 Pattern de traitement d'événements avec groupement par hash (Moyenne duplication)

Localisation : lib/userContentSync.ts

Fonctions concernées :

  • fetchAndCachePublications (lignes 88-126)
  • fetchAndCacheSeries (lignes 218-256)

Pattern dupliqué :

// Group events by hash ID and cache the latest version of each
const eventsByHashId = new Map<string, Event[]>()
for (const event of events) {
  const tags = extractTagsFromEvent(event)
  if (tags.id) {
    const parsed = parseObjectId(tags.id)
    const hash = parsed.hash ?? tags.id
    if (!eventsByHashId.has(hash)) {
      eventsByHashId.set(hash, [])
    }
    eventsByHashId.get(hash)!.push(event)
  }
}

// Cache each publication/series
for (const [_hash, hashEvents] of eventsByHashId.entries()) {
  const latestEvent = getLatestVersion(hashEvents)
  if (latestEvent) {
    const extracted = await extractPublicationFromEvent(latestEvent) // ou extractSeriesFromEvent
    if (extracted) {
      const publicationParsed = parseObjectId(extracted.id)
      const extractedHash = publicationParsed.hash ?? extracted.id
      const extractedIndex = publicationParsed.index ?? 0
      const tags = extractTagsFromEvent(latestEvent)
      const { writeService } = await import('./writeService')
      await writeService.writeObject(
        'publication', // ou 'series'
        extractedHash,
        latestEvent,
        extracted,
        tags.version ?? 0,
        tags.hidden ?? false,
        extractedIndex,
        false
      )
    }
  }
}

Impact :

  • ~40 lignes dupliquées
  • Logique de groupement et cache répétée

Solution proposée : Créer une fonction générique groupAndCacheEventsByHash dans lib/eventCacheHelper.ts


1.4 Pattern de queries avec objectCache (Faible duplication mais répétitif)

Localisation : Fichiers *Queries.ts

Fichiers concernés :

  • lib/purchaseQueries.ts
  • lib/seriesQueries.ts
  • lib/articleQueries.ts
  • (et probablement d'autres fichiers queries)

Pattern répétitif :

export async function getXxxById(id: string, _timeoutMs: number = 5000): Promise<Xxx | null> {
  const parsed = parseObjectId(id)
  const hash = parsed.hash ?? id

  // Read only from IndexedDB cache
  const cached = await objectCache.get('xxx', hash)
  if (cached) {
    return cached as Xxx
  }

  // Also try by ID if hash lookup failed
  const cachedById = await objectCache.getById('xxx', id)
  if (cachedById) {
    return cachedById as Xxx
  }

  // Not found in cache - return null (no network request)
  return null
}

Impact :

  • Pattern répété dans plusieurs fichiers queries
  • Logique de fallback identique

Solution proposée : Créer une fonction helper getCachedObjectById dans lib/queryHelpers.ts


1.5 Pattern de writeObject avec extraction (Moyenne duplication)

Localisation : Multiple fichiers

Fichiers concernés :

  • lib/userContentSync.ts (lignes 107-123, 237-253, 351-359, 453-461, 555-563)
  • lib/platformSync.ts (lignes 304-305, 310-311, 316-317)

Pattern répétitif :

const extracted = await extractXxxFromEvent(event)
if (extracted) {
  const { writeService } = await import('./writeService')
  await writeService.writeObject(
    'xxx',
    extracted.hash,
    event,
    extracted,
    tags.version ?? 0,
    tags.hidden ?? false,
    extracted.index ?? 0,
    false
  )
}

Impact :

  • Appels répétés avec mêmes paramètres par défaut
  • Logique d'extraction + écriture répétée

Solution proposée : Créer une fonction helper cacheEventAsObject dans lib/eventCacheHelper.ts


1.6 Pattern de gestion de transactions IndexedDB (Moyenne duplication)

Localisation : Multiple fichiers

Pattern répétitif :

const transaction = db.transaction([STORE_NAME], 'readonly' | 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const index = store.index('xxx')

return new Promise((resolve, reject) => {
  const request = index.get(key) // ou openCursor, getAll, etc.
  request.onsuccess = () => resolve(request.result)
  request.onerror = () => reject(request.error)
})

Impact :

  • Wrapper Promise répété dans de nombreux endroits
  • Gestion d'erreurs similaire partout

Solution proposée : Créer des helpers dans lib/indexedDBHelper.ts : getFromStore, getAllFromStore, putToStore, deleteFromStore, openCursor


1.7 Pattern de gestion de progress dans SyncProgressBar (Duplication interne)

Localisation : components/SyncProgressBar.tsx

Pattern dupliqué (lignes 104-126 et 177-199) :

const { syncProgressManager } = await import('@/lib/syncProgressManager')
const checkProgress = (): void => {
  const currentProgress = syncProgressManager.getProgress()
  if (currentProgress) {
    setSyncProgress(currentProgress)
    if (currentProgress.completed) {
      setIsSyncing(false)
      void loadSyncStatus()
    }
  }
}
// Check progress periodically
const progressInterval = setInterval(() => {
  checkProgress()
  const currentProgress = syncProgressManager.getProgress()
  if (currentProgress?.completed) {
    clearInterval(progressInterval)
  }
}, 500)
// Cleanup after 60 seconds max
setTimeout(() => {
  clearInterval(progressInterval)
  setIsSyncing(false)
}, 60000)

Impact :

  • Code dupliqué dans deux fonctions (auto-sync et resynchronize)
  • Logique de polling répétée

Solution proposée : Extraire dans un hook custom useSyncProgress ou une fonction helper


2. Opportunités de mutualisation/centralisation

2.1 Service d'initialisation IndexedDB unifié

Objectif : Centraliser toute la logique d'initialisation IndexedDB

Structure proposée :

// lib/indexedDBHelper.ts
export interface IndexedDBConfig {
  dbName: string
  version: number
  storeName: string
  keyPath: string
  indexes?: Array<{ name: string; keyPath: string; unique?: boolean }>
  onUpgrade?: (db: IDBDatabase, event: IDBVersionChangeEvent) => void
}

export class IndexedDBHelper {
  private static instances = new Map<string, IndexedDBHelper>()
  private db: IDBDatabase | null = null
  private initPromise: Promise<void> | null = null

  static getInstance(config: IndexedDBConfig): IndexedDBHelper {
    // Singleton par dbName
  }

  async init(): Promise<IDBDatabase> { /* ... */ }
  async getStore(mode: 'readonly' | 'readwrite'): Promise<IDBObjectStore> { /* ... */ }
  // Helpers pour opérations courantes
}

Bénéfices :

  • Réduction de ~200 lignes de code dupliqué
  • Maintenance centralisée
  • Cohérence garantie entre services

2.2 Helper de subscription avec relay rotation

Objectif : Centraliser le pattern de subscription avec rotation de relais

Structure proposée :

// lib/syncSubscriptionHelper.ts
export interface SyncSubscriptionConfig {
  pool: SimplePoolWithSub
  filters: Filter[]
  onEvent: (event: Event) => void | Promise<void>
  onComplete?: (events: Event[]) => void | Promise<void>
  timeout?: number
  updateProgress?: (relayUrl: string) => void
}

export async function createSyncSubscription(
  config: SyncSubscriptionConfig
): Promise<{ subscription: Subscription; relayUrl: string; events: Event[] }> {
  // Centralise toute la logique de rotation, gestion d'événements, timeout
}

Bénéfices :

  • Réduction de ~300 lignes de code dupliqué
  • Gestion d'erreurs unifiée
  • Facilite les tests

2.3 Helper de groupement et cache d'événements

Objectif : Centraliser la logique de groupement par hash et cache

Structure proposée :

// lib/eventCacheHelper.ts
export interface EventCacheConfig {
  objectType: ObjectType
  extractor: (event: Event) => Promise<ExtractedObject | null>
  getHash: (extracted: ExtractedObject) => string
  getIndex: (extracted: ExtractedObject) => number
}

export async function groupAndCacheEventsByHash(
  events: Event[],
  config: EventCacheConfig
): Promise<void> {
  // Groupement par hash, sélection de la dernière version, cache
}

Bénéfices :

  • Réduction de ~40 lignes de code dupliqué
  • Logique de versioning centralisée

2.4 Helper de queries unifié

Objectif : Simplifier les queries avec fallback hash/ID

Structure proposée :

// lib/queryHelpers.ts
export async function getCachedObjectById<T>(
  objectType: ObjectType,
  id: string
): Promise<T | null> {
  // Logique de fallback hash/ID centralisée
}

Bénéfices :

  • Réduction de code répétitif dans les queries
  • Cohérence des fallbacks

2.5 Helper de cache d'événements

Objectif : Simplifier l'écriture d'objets après extraction

Structure proposée :

// lib/eventCacheHelper.ts
export async function cacheEventAsObject(
  event: Event,
  objectType: ObjectType,
  extractor: (event: Event) => Promise<ExtractedObject | null>
): Promise<boolean> {
  // Extraction + écriture avec paramètres par défaut
}

Bénéfices :

  • Réduction de code répétitif
  • Paramètres par défaut cohérents

3. Axes d'organisation et d'optimisation

3.1 Organisation des helpers

Structure proposée :

lib/
  helpers/
    indexedDBHelper.ts      # Initialisation et opérations IndexedDB
    syncSubscriptionHelper.ts # Subscriptions avec relay rotation
    eventCacheHelper.ts      # Groupement et cache d'événements
    queryHelpers.ts          # Helpers pour queries
    transactionHelpers.ts    # Wrappers pour transactions IndexedDB

Bénéfices :

  • Organisation claire par responsabilité
  • Facilite la découverte et la réutilisation
  • Séparation des préoccupations

3.2 Optimisation des imports dynamiques

Problème identifié : Imports dynamiques répétés dans les boucles

Exemples :

  • const { writeService } = await import('./writeService') dans des boucles
  • const { syncProgressManager } = await import('./syncProgressManager') dans des callbacks

Solution : Importer en début de fonction ou utiliser des imports statiques quand possible

Impact : Réduction des latences et amélioration des performances


3.3 Optimisation de la gestion des événements

Problème identifié : Accumulation d'événements en mémoire avant traitement

Solution : Traitement en streaming avec backpressure

Structure proposée :

export async function createStreamingSyncSubscription<T>(
  config: SyncSubscriptionConfig & {
    processor: (event: Event) => Promise<T>
    batchSize?: number
  }
): Promise<{ results: T[] }> {
  // Traitement par batch au lieu d'accumulation complète
}

Bénéfices :

  • Réduction de l'utilisation mémoire
  • Traitement plus rapide pour de gros volumes

3.4 Centralisation de la gestion d'erreurs IndexedDB

Problème identifié : Gestion d'erreurs dispersée et parfois incohérente

Solution : Créer un wrapper d'erreur IndexedDB avec logging structuré

Structure proposée :

// lib/indexedDBHelper.ts
export class IndexedDBError extends Error {
  constructor(
    message: string,
    public readonly operation: string,
    public readonly storeName?: string,
    public readonly cause?: unknown
  ) {
    super(message)
    // Logging structuré automatique
  }
}

Bénéfices :

  • Traçabilité améliorée
  • Gestion d'erreurs cohérente

3.5 Optimisation des transactions IndexedDB

Problème identifié : Transactions multiples pour des opérations liées

Solution : Regrouper les opérations dans une seule transaction quand possible

Exemple : Dans writeWorker.js, handleWriteMultiTable pourrait optimiser les transactions par type


3.6 Typage strict pour les helpers

Problème identifié : Utilisation de unknown et any dans certains helpers

Solution : Génériques TypeScript stricts pour tous les helpers

Bénéfices :

  • Sécurité de type améliorée
  • Meilleure autocomplétion
  • Détection d'erreurs à la compilation

3.7 Documentation et exemples

Problème identifié : Manque de documentation sur les patterns à utiliser

Solution : Créer docs/patterns/ avec :

  • indexedDB-patterns.md : Patterns d'utilisation IndexedDB
  • subscription-patterns.md : Patterns de subscription
  • caching-patterns.md : Patterns de cache

Bénéfices :

  • Onboarding facilité
  • Cohérence des implémentations futures

4. Priorisation des actions

Priorité 1 (Critique - Impact élevé)

  1. Service d'initialisation IndexedDB unifié (~200 lignes économisées)
  2. Helper de subscription avec relay rotation (~300 lignes économisées)

Priorité 2 (Important - Impact moyen)

  1. Helper de groupement et cache d'événements (~40 lignes économisées)
  2. Helper de cache d'événements (réduction de code répétitif)
  3. Helper de queries unifié (simplification des queries)

Priorité 3 (Amélioration - Impact faible mais bénéfique)

  1. Optimisation des imports dynamiques
  2. Centralisation de la gestion d'erreurs
  3. Documentation des patterns

5. Risques et précautions

Risques identifiés

  1. Régression fonctionnelle : Refactoring de code critique (IndexedDB, subscriptions)

    • Mitigation : Tests unitaires avant refactoring, migration progressive
  2. Performance : Abstraction peut introduire overhead

    • Mitigation : Benchmarks avant/après, optimisation si nécessaire
  3. Compatibilité : Changements d'API peuvent casser le code existant

    • Mitigation : Déprecation progressive, migration guides

Précautions

  • Valider chaque refactoring avec des tests
  • Maintenir la rétrocompatibilité quand possible
  • Documenter les breaking changes
  • Mesurer l'impact sur les performances

6. Métriques de succès

Réduction de code

  • Objectif : Réduction de ~600 lignes de code dupliqué
  • Mesure : Comparaison avant/après avec cloc ou similaire

Maintenabilité

  • Objectif : Réduction du temps de modification de patterns communs
  • Mesure : Temps moyen pour appliquer un changement (avant/après)

Qualité

  • Objectif : Réduction des bugs liés à l'incohérence
  • Mesure : Nombre de bugs liés à la duplication (avant/après)

Conclusion

Cette analyse identifie des opportunités significatives de réduction de duplication et d'amélioration de l'organisation du code. Les priorités 1 et 2 devraient être traitées en premier pour maximiser l'impact sur la maintenabilité et réduire les risques d'incohérences.

Les refactorings proposés respectent l'architecture existante et les principes de séparation des responsabilités. Ils doivent être réalisés progressivement avec validation à chaque étape.