diff --git a/docs/patterns/caching-patterns.md b/docs/patterns/caching-patterns.md new file mode 100644 index 0000000..4627b8c --- /dev/null +++ b/docs/patterns/caching-patterns.md @@ -0,0 +1,175 @@ +# Patterns de cache d'événements + +**Date** : 2024-12-19 +**Auteur** : Équipe 4NK + +## Introduction + +Ce document décrit les patterns à utiliser pour cacher des événements Nostr dans IndexedDB. + +## Pattern recommandé : Utilisation des helpers de cache + +### Cache d'événements groupés par hash + +Pour les objets qui ont plusieurs versions (publications, series), utiliser `groupAndCacheEventsByHash` : + +```typescript +import { groupAndCacheEventsByHash } from '@/lib/helpers/eventCacheHelper' +import { extractPublicationFromEvent } from '@/lib/metadataExtractor' +import { parseObjectId } from '@/lib/urlGenerator' +import { extractTagsFromEvent } from '@/lib/nostrTagSystem' + +await groupAndCacheEventsByHash(events, { + objectType: 'publication', + extractor: extractPublicationFromEvent, + getHash: (extracted: unknown): string | null => { + const id = (extracted as { id?: string })?.id + if (!id) { + return null + } + const parsed = parseObjectId(id) + return parsed.hash ?? id + }, + getIndex: (extracted: unknown): number => { + return (extracted as { index?: number })?.index ?? 0 + }, + getVersion: (event: Event): number => { + return extractTagsFromEvent(event).version ?? 0 + }, + getHidden: (event: Event): boolean => { + return extractTagsFromEvent(event).hidden ?? false + }, +}) +``` + +### Cache d'événements simples + +Pour les objets sans versioning (purchases, sponsoring, review tips), utiliser `writeObjectToCache` : + +```typescript +import { writeObjectToCache } from '@/lib/helpers/writeObjectHelper' + +await writeObjectToCache({ + objectType: 'purchase', + hash: purchase.hash, + event, + parsed: purchase, + index: purchase.index, +}) +``` + +### Cache avec extraction automatique + +Pour combiner extraction et cache en une seule opération : + +```typescript +import { extractAndWriteObject } from '@/lib/helpers/writeObjectHelper' +import { extractPurchaseFromEvent } from '@/lib/metadataExtractor' + +const cached = await extractAndWriteObject( + event, + 'purchase', + extractPurchaseFromEvent +) + +if (cached) { + console.log('Event cached successfully') +} +``` + +## Pattern de cache dans les boucles + +### Optimisation : Cache l'import de writeService + +Le helper `writeObjectToCache` cache automatiquement l'import de `writeService`, donc pas besoin d'optimiser manuellement : + +```typescript +// ✅ Bon : writeObjectToCache gère le cache automatiquement +for (const event of events) { + await writeObjectToCache({ + objectType: 'purchase', + hash: purchase.hash, + event, + parsed: purchase, + }) +} + +// ❌ Éviter : Import répété dans la boucle +for (const event of events) { + const { writeService } = await import('./writeService') + await writeService.writeObject(...) +} +``` + +## Gestion des versions + +Pour les objets avec versioning, `groupAndCacheEventsByHash` : +1. Groupe les événements par hash +2. Sélectionne la dernière version avec `getLatestVersion` +3. Cache uniquement la dernière version + +```typescript +// Les événements sont automatiquement groupés par hash +// Seule la dernière version est cachée +await groupAndCacheEventsByHash(events, { + objectType: 'publication', + extractor: extractPublicationFromEvent, + // ... config +}) +``` + +## Bonnes pratiques + +1. **Utiliser les helpers appropriés** : + - `groupAndCacheEventsByHash` pour objets avec versioning + - `writeObjectToCache` pour objets simples + - `extractAndWriteObject` pour combiner extraction et cache + +2. **Ne pas dupliquer la logique de cache** : Toujours utiliser les helpers centralisés + +3. **Gérer les erreurs** : Les helpers propagent les erreurs, les gérer en amont + +4. **Typage strict** : Utiliser les types TypeScript pour `parsed` + +5. **Performance** : Les helpers optimisent automatiquement les imports + +## Exemples complets + +### Cache de publications avec groupement + +```typescript +import { groupAndCacheEventsByHash } from '@/lib/helpers/eventCacheHelper' +import { cachePublicationsByHash } from '@/lib/helpers/syncContentCacheHelpers' + +// Utiliser le helper spécialisé +await cachePublicationsByHash(events) +``` + +### Cache de purchases + +```typescript +import { cachePurchases } from '@/lib/helpers/syncCacheHelpers' + +await cachePurchases(events) +``` + +### Cache personnalisé + +```typescript +import { writeObjectToCache } from '@/lib/helpers/writeObjectHelper' + +for (const event of events) { + const extracted = await extractMyObjectFromEvent(event) + if (extracted?.hash) { + await writeObjectToCache({ + objectType: 'my_object', + hash: extracted.hash, + event, + parsed: extracted, + version: extractTagsFromEvent(event).version, + hidden: extractTagsFromEvent(event).hidden, + index: extracted.index, + }) + } +} +``` diff --git a/docs/patterns/indexedDB-patterns.md b/docs/patterns/indexedDB-patterns.md new file mode 100644 index 0000000..9ac5c5c --- /dev/null +++ b/docs/patterns/indexedDB-patterns.md @@ -0,0 +1,181 @@ +# Patterns d'utilisation IndexedDB + +**Date** : 2024-12-19 +**Auteur** : Équipe 4NK + +## Introduction + +Ce document décrit les patterns à utiliser pour interagir avec IndexedDB dans le projet. Tous les services IndexedDB doivent utiliser `IndexedDBHelper` pour garantir la cohérence et réduire la duplication. + +## Pattern recommandé : Utilisation de IndexedDBHelper + +### Création d'un service IndexedDB + +```typescript +import { createIndexedDBHelper } from '@/lib/helpers/indexedDBHelper' + +const dbHelper = createIndexedDBHelper({ + dbName: 'my_database', + version: 1, + storeName: 'my_store', + keyPath: 'id', + indexes: [ + { name: 'timestamp', keyPath: 'timestamp', unique: false }, + { name: 'type', keyPath: 'type', unique: false }, + ], + onUpgrade: (db, event) => { + // Migration logic if needed + }, +}) + +// Utilisation +await dbHelper.init() +const store = await dbHelper.getStore('readwrite') +await dbHelper.add({ id: '1', data: 'value' }) +``` + +### Opérations courantes + +#### Ajouter un objet + +```typescript +await dbHelper.add(object) +``` + +#### Récupérer un objet par clé + +```typescript +const obj = await dbHelper.get(key) +``` + +#### Récupérer par index + +```typescript +const obj = await dbHelper.getByIndex('indexName', value) +``` + +#### Récupérer tous les objets + +```typescript +const all = await dbHelper.getAll() +``` + +#### Compter par index + +```typescript +const count = await dbHelper.countByIndex('indexName', IDBKeyRange.only(value)) +``` + +#### Mettre à jour un objet + +```typescript +await dbHelper.update(object) +``` + +#### Supprimer un objet + +```typescript +await dbHelper.delete(key) +``` + +#### Supprimer tous les objets + +```typescript +await dbHelper.clear() +``` + +## Gestion des erreurs + +Toutes les erreurs IndexedDB sont automatiquement wrappées dans `IndexedDBError` avec : +- Message d'erreur +- Nom de l'opération +- Nom du store (si applicable) +- Cause originale + +```typescript +try { + await dbHelper.add(object) +} catch (error) { + if (error instanceof IndexedDBError) { + console.error(`Operation: ${error.operation}, Store: ${error.storeName}`) + } + throw error +} +``` + +## Transactions + +Les transactions sont gérées automatiquement par `IndexedDBHelper`. Chaque opération utilise une transaction appropriée. + +Pour des opérations multiples dans une seule transaction : + +```typescript +const store = await dbHelper.getStore('readwrite') +// Toutes les opérations sur store sont dans la même transaction +await store.add(object1) +await store.add(object2) +// Transaction se ferme automatiquement à la fin de la fonction +``` + +## Migrations + +Les migrations sont gérées dans `onUpgrade` : + +```typescript +const dbHelper = createIndexedDBHelper({ + // ... + onUpgrade: (db, event) => { + const transaction = event.target.transaction + const store = transaction.objectStore('my_store') + + // Ajouter un index si nécessaire + if (!store.indexNames.contains('newIndex')) { + store.createIndex('newIndex', 'newField', { unique: false }) + } + }, +}) +``` + +## Bonnes pratiques + +1. **Toujours utiliser IndexedDBHelper** : Ne pas créer de code d'initialisation IndexedDB personnalisé +2. **Gérer les erreurs** : Toujours utiliser try/catch avec IndexedDBError +3. **Typage strict** : Utiliser les génériques TypeScript pour le typage +4. **Transactions** : Regrouper les opérations liées dans une seule transaction +5. **Migrations** : Tester les migrations sur des données réelles avant déploiement + +## Exemples complets + +### Service de cache simple + +```typescript +import { createIndexedDBHelper } from '@/lib/helpers/indexedDBHelper' + +class MyCacheService { + private dbHelper = createIndexedDBHelper({ + dbName: 'my_cache', + version: 1, + storeName: 'items', + keyPath: 'id', + indexes: [ + { name: 'timestamp', keyPath: 'timestamp', unique: false }, + ], + }) + + async init(): Promise { + await this.dbHelper.init() + } + + async addItem(item: MyItem): Promise { + await this.dbHelper.add(item) + } + + async getItem(id: string): Promise { + return await this.dbHelper.get(id) + } + + async getAllItems(): Promise { + return await this.dbHelper.getAll() + } +} +``` diff --git a/docs/patterns/subscription-patterns.md b/docs/patterns/subscription-patterns.md new file mode 100644 index 0000000..0f51608 --- /dev/null +++ b/docs/patterns/subscription-patterns.md @@ -0,0 +1,152 @@ +# Patterns de subscription Nostr + +**Date** : 2024-12-19 +**Auteur** : Équipe 4NK + +## Introduction + +Ce document décrit les patterns à utiliser pour créer des subscriptions Nostr avec rotation de relais et gestion des événements. + +## Pattern recommandé : Utilisation de createSyncSubscription + +### Subscription simple avec relay rotation + +```typescript +import { createSyncSubscription } from '@/lib/helpers/syncSubscriptionHelper' +import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' +import type { Filter } from 'nostr-tools' + +async function fetchEvents( + pool: SimplePoolWithSub, + filters: Filter[] +): Promise { + const result = await createSyncSubscription({ + pool, + filters, + timeout: 10000, + }) + + return result.events +} +``` + +### Subscription avec filtrage d'événements + +```typescript +const result = await createSyncSubscription({ + pool, + filters, + eventFilter: (event: Event): boolean => { + const tags = extractTagsFromEvent(event) + return tags.type === 'publication' && !tags.hidden + }, + timeout: 10000, +}) +``` + +### Subscription avec callbacks + +```typescript +const result = await createSyncSubscription({ + pool, + filters, + onEvent: (event: Event): void => { + console.log('Received event:', event.id) + // Traitement immédiat si nécessaire + }, + onComplete: async (events: Event[]): Promise => { + console.log(`Received ${events.length} events`) + // Traitement final de tous les événements + }, + timeout: 10000, +}) +``` + +### Subscription avec mise à jour du progrès + +```typescript +const result = await createSyncSubscription({ + pool, + filters, + updateProgress: (relayUrl: string): void => { + // Mise à jour personnalisée du progrès + console.log('Using relay:', relayUrl) + }, + timeout: 10000, +}) +``` + +## Gestion des erreurs + +Les erreurs de subscription sont gérées automatiquement : +- Rotation automatique vers le relais suivant en cas d'échec +- Fallback vers le relais principal si tous les relais échouent +- Timeout automatique après le délai spécifié + +```typescript +try { + const result = await createSyncSubscription({ + pool, + filters, + timeout: 10000, + }) + // Traiter les événements +} catch (error) { + console.error('Subscription failed:', error) + // Gérer l'erreur +} +``` + +## Bonnes pratiques + +1. **Toujours utiliser createSyncSubscription** : Ne pas créer de code de subscription personnalisé +2. **Définir un timeout approprié** : Par défaut 10 secondes, ajuster selon le contexte +3. **Filtrer les événements** : Utiliser `eventFilter` pour éviter de traiter des événements non pertinents +4. **Gérer les callbacks** : Utiliser `onEvent` pour traitement immédiat, `onComplete` pour traitement final +5. **Mise à jour du progrès** : Utiliser `updateProgress` pour informer l'utilisateur + +## Exemples complets + +### Synchronisation de publications + +```typescript +import { createSyncSubscription } from '@/lib/helpers/syncSubscriptionHelper' +import { buildTagFilter } from '@/lib/nostrTagSystemFilter' +import { PLATFORM_SERVICE } from '@/lib/platformConfig' +import { extractTagsFromEvent } from '@/lib/nostrTagSystem' + +async function syncPublications( + pool: SimplePoolWithSub, + authorPubkey: string +): Promise { + const { getLastSyncDate } = await import('./syncStorage') + const lastSyncDate = await getLastSyncDate() + + const filters = [ + { + ...buildTagFilter({ + type: 'publication', + authorPubkey, + service: PLATFORM_SERVICE, + }), + since: lastSyncDate, + limit: 1000, + }, + ] + + const result = await createSyncSubscription({ + pool, + filters, + eventFilter: (event: Event): boolean => { + const tags = extractTagsFromEvent(event) + return tags.type === 'publication' && !tags.hidden + }, + timeout: 10000, + }) + + // Traiter les événements + for (const event of result.events) { + // ... + } +} +``` diff --git a/lib/helpers/writeObjectHelper.ts b/lib/helpers/writeObjectHelper.ts index e59f670..58ed038 100644 --- a/lib/helpers/writeObjectHelper.ts +++ b/lib/helpers/writeObjectHelper.ts @@ -79,14 +79,16 @@ export async function extractAndWriteObject Promise }).initDB(objectType) - const transaction = db.transaction(['objects'], 'readonly') - const store = transaction.objectStore('objects') - const request = store.openCursor() - - const found = await new Promise((resolve, reject) => { - request.onsuccess = (event: globalThis.Event): void => { - const cursor = (event.target as IDBRequest).result - if (cursor) { - const obj = cursor.value as { id: string; event: { id: string } } - if (obj.event.id === eventId) { - resolve(obj.id) - return - } - cursor.continue() - } else { - resolve(null) - } - } - request.onerror = (): void => { - reject(request.error) - } + // Use getAll to search all objects + const allObjects = await objectCache.getAll(objectType) + const matching = allObjects.find((obj) => { + const cachedObj = obj as CachedObject + return cachedObj.event?.id === eventId }) - - if (found) { - await writeService.updatePublished(objectType, found, published) + if (matching) { + const cachedObj = matching as CachedObject + await writeService.updatePublished(objectType, cachedObj.id, published) return } } catch (error) { diff --git a/lib/userContentSync.ts b/lib/userContentSync.ts index 5a264a3..9e085f3 100644 --- a/lib/userContentSync.ts +++ b/lib/userContentSync.ts @@ -4,6 +4,7 @@ */ import { nostrService } from './nostr' +import type { SimplePool } from 'nostr-tools' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { SyncProgress } from './helpers/syncProgressHelper' import { initializeSyncProgress, finalizeSync } from './helpers/syncProgressHelper' @@ -30,7 +31,7 @@ export async function syncUserContentToCache( onProgress?: (progress: SyncProgress) => void ): Promise { try { - const pool = nostrService.getPool() + const pool: SimplePool | null = (nostrService as { getPool: () => SimplePool | null }).getPool() if (!pool) { const errorMsg = 'Pool not initialized, cannot sync user content' console.warn(errorMsg) diff --git a/public/writeWorker.js b/public/writeWorker.js index 806dcac..cfae019 100644 --- a/public/writeWorker.js +++ b/public/writeWorker.js @@ -72,10 +72,10 @@ async function executeWriteTask(task) { await handleCreateNotification(data, id) break case 'LOG_PUBLICATION': - await handleLogPublication(data, id) + await handleLogPublication(data) break case 'WRITE_MULTI_TABLE': - await handleWriteMultiTable(data, id) + await handleWriteMultiTable(data) break default: throw new Error(`Unknown message type: ${type}`) @@ -121,7 +121,7 @@ async function handleWriteObject(data, taskId) { const store = transaction.objectStore('objects') // Vérifier si l'objet existe déjà pour préserver published - const existing = await executeTransactionOperation(store, (s) => s.get(finalId)).catch((_e) => null) + const existing = await executeTransactionOperation(store, (s) => s.get(finalId)).catch(() => null) // Préserver published si existant et non fourni const finalPublished = existing && published === false ? existing.published : (published ?? false) @@ -218,7 +218,7 @@ async function handleUpdatePublished(data, taskId) { * Handle write multi-table request * Transactions multi-tables : plusieurs transactions, logique de découpage côté worker */ -async function handleWriteMultiTable(data, _taskId) { +async function handleWriteMultiTable(data) { const { writes } = data // Array of { objectType, hash, event, parsed, version, hidden, index, published } try { @@ -251,7 +251,7 @@ async function handleWriteMultiTable(data, _taskId) { finalId = `${hash}:${count}:${version}` } - const existing = await executeTransactionOperation(store, (s) => s.get(finalId)).catch((_e) => null) + const existing = await executeTransactionOperation(store, (s) => s.get(finalId)).catch(() => null) const finalPublished = existing && published === false ? existing.published : (published ?? false) @@ -302,7 +302,7 @@ async function handleCreateNotification(data, taskId) { // Vérifier si la notification existe déjà const index = store.index('eventId') - const existing = await executeTransactionOperation(index, (idx) => idx.get(eventId)).catch((_e) => null) + const existing = await executeTransactionOperation(index, (idx) => idx.get(eventId)).catch(() => null) if (existing) { // Notification déjà existante @@ -344,7 +344,7 @@ async function handleCreateNotification(data, taskId) { /** * Handle log publication request */ -async function handleLogPublication(data, _taskId) { +async function handleLogPublication(data) { const { eventId, relayUrl, success, error, objectType, objectId } = data try { @@ -418,7 +418,7 @@ function openDB(objectType) { * Open IndexedDB for notifications */ function openNotificationDB() { - return openIndexedDB('nostr_notifications', 1, (db, _event) => { + return openIndexedDB('nostr_notifications', 1, (db) => { if (!db.objectStoreNames.contains('notifications')) { const store = db.createObjectStore('notifications', { keyPath: 'id' }) store.createIndex('type', 'type', { unique: false })