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.tslib/seriesQueries.tslib/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 bouclesconst { 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 IndexedDBsubscription-patterns.md: Patterns de subscriptioncaching-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é)
- Service d'initialisation IndexedDB unifié (~200 lignes économisées)
- Helper de subscription avec relay rotation (~300 lignes économisées)
Priorité 2 (Important - Impact moyen)
- Helper de groupement et cache d'événements (~40 lignes économisées)
- Helper de cache d'événements (réduction de code répétitif)
- Helper de queries unifié (simplification des queries)
Priorité 3 (Amélioration - Impact faible mais bénéfique)
- Optimisation des imports dynamiques
- Centralisation de la gestion d'erreurs
- Documentation des patterns
5. Risques et précautions
Risques identifiés
-
Régression fonctionnelle : Refactoring de code critique (IndexedDB, subscriptions)
- Mitigation : Tests unitaires avant refactoring, migration progressive
-
Performance : Abstraction peut introduire overhead
- Mitigation : Benchmarks avant/après, optimisation si nécessaire
-
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
clocou 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.