/** * Web Worker for writing to IndexedDB * Handles all write operations to IndexedDB to keep main thread responsive * * - Reçoit les données complètes (pas seulement les modifications) * - Gère une pile d'écritures pour éviter les conflits * - Transactions multi-tables : plusieurs transactions, logique de découpage côté worker */ const DB_VERSIONS = { author: 3, series: 3, publication: 3, review: 3, purchase: 3, sponsoring: 3, review_tip: 3, payment_note: 3, } // Pile d'écritures pour éviter les conflits const writeQueue = [] let processingQueue = false /** * Process the write queue sequentially */ async function processQueue() { if (processingQueue || writeQueue.length === 0) { return } processingQueue = true while (writeQueue.length > 0) { const task = writeQueue.shift() if (!task) { continue } try { await executeWriteTask(task) } catch (error) { self.postMessage({ type: 'ERROR', data: { error: error instanceof Error ? error.message : String(error), originalType: task.type, taskId: task.id, }, }) } } processingQueue = false } /** * Execute a write task */ async function executeWriteTask(task) { const { type, data, id } = task switch (type) { case 'WRITE_OBJECT': await handleWriteObject(data, id) break case 'UPDATE_PUBLISHED': await handleUpdatePublished(data, id) break case 'CREATE_NOTIFICATION': await handleCreateNotification(data, id) break case 'LOG_PUBLICATION': await handleLogPublication(data) break case 'WRITE_MULTI_TABLE': await handleWriteMultiTable(data) break default: throw new Error(`Unknown message type: ${type}`) } } // Listen for messages from main thread self.addEventListener('message', (event) => { // event is used to access event.data const { type, data } = event.data // Add to queue with unique ID const taskId = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` writeQueue.push({ type, data, id: taskId }) // Process queue void processQueue() }) /** * Handle write object request * Reçoit les données complètes */ async function handleWriteObject(data, taskId) { const { objectType, hash, event, parsed, version, hidden, index, published } = data try { // Open IndexedDB const db = await openDB(objectType) // Calculer l'ID complet si nécessaire let finalId = hash if (index !== undefined && index !== null) { // Construire l'ID avec hash, index et version finalId = `${hash}:${index}:${version}` } else { // Compter les objets avec le même hash pour déterminer l'index const count = await countObjectsWithHash(db, hash) finalId = `${hash}:${count}:${version}` } const transaction = db.transaction(['objects'], 'readwrite') 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(() => null) // Préserver published si existant et non fourni const finalPublished = existing && published === false ? existing.published : (published ?? false) const object = { id: finalId, hash, hashId: hash, // Legacy field index: index ?? (existing?.index ?? (finalId.split(':')[1] ? parseInt(finalId.split(':')[1]) : 0)), event, parsed, version, hidden, published: finalPublished, createdAt: event.created_at, cachedAt: Date.now(), pubkey: event.pubkey, } await executeTransactionOperation(store, (s) => s.put(object)) self.postMessage({ type: 'WRITE_OBJECT_SUCCESS', data: { hash, id: finalId, taskId }, }) } catch (error) { throw new Error(`Failed to write object: ${error.message}`) } } /** * Count objects with same hash */ function countObjectsWithHash(db, hash) { return new Promise((resolve, reject) => { const transaction = db.transaction(['objects'], 'readonly') const store = transaction.objectStore('objects') const index = store.index('hash') const request = index.openCursor(IDBKeyRange.only(hash)) let count = 0 request.onsuccess = (event) => { const cursor = event.target.result if (cursor) { count++ cursor.continue() } else { resolve(count) } } request.onerror = () => reject(request.error) }) } /** * Handle update published status request */ async function handleUpdatePublished(data, taskId) { const { objectType, id, published } = data try { const db = await openDB(objectType) const transaction = db.transaction(['objects'], 'readwrite') const store = transaction.objectStore('objects') const existing = await executeTransactionOperation(store, (s) => s.get(id)) if (!existing) { throw new Error(`Object ${id} not found`) } const updated = { ...existing, published, } await new Promise((resolve, reject) => { const request = store.put(updated) request.onsuccess = () => resolve() request.onerror = () => reject(request.error) }) self.postMessage({ type: 'UPDATE_PUBLISHED_SUCCESS', data: { id, taskId }, }) } catch (error) { throw new Error(`Failed to update published status: ${error.message}`) } } /** * Handle write multi-table request * Transactions multi-tables : plusieurs transactions, logique de découpage côté worker */ async function handleWriteMultiTable(data) { const { writes } = data // Array of { objectType, hash, event, parsed, version, hidden, index, published } try { // Grouper par objectType pour créer des transactions par type const writesByType = new Map() for (const write of writes) { const { objectType } = write if (!writesByType.has(objectType)) { writesByType.set(objectType, []) } writesByType.get(objectType).push(write) } // Exécuter les transactions par type (plusieurs transactions) const results = [] for (const [objectType, typeWrites] of writesByType) { const db = await openDB(objectType) const transaction = db.transaction(['objects'], 'readwrite') const store = transaction.objectStore('objects') for (const write of typeWrites) { const { hash, event, parsed, version, hidden, index, published } = write // Calculer l'ID (logique de découpage côté worker) let finalId = hash if (index !== undefined && index !== null) { finalId = `${hash}:${index}:${version}` } else { const count = await countObjectsWithHash(db, hash) finalId = `${hash}:${count}:${version}` } const existing = await executeTransactionOperation(store, (s) => s.get(finalId)).catch(() => null) const finalPublished = existing && published === false ? existing.published : (published ?? false) const object = { id: finalId, hash, hashId: hash, index: index ?? (existing?.index ?? 0), event, parsed, version, hidden, published: finalPublished, createdAt: event.created_at, cachedAt: Date.now(), pubkey: event.pubkey, } await new Promise((resolve, reject) => { const request = store.put(object) request.onsuccess = () => resolve() request.onerror = () => reject(request.error) }) results.push({ objectType, id: finalId, hash }) } } self.postMessage({ type: 'WRITE_MULTI_TABLE_SUCCESS', data: { results }, }) } catch (error) { throw new Error(`Failed to write multi-table: ${error.message}`) } } /** * Handle create notification request */ async function handleCreateNotification(data, taskId) { const { type, objectType, objectId, eventId, notificationData } = data try { const db = await openNotificationDB() const transaction = db.transaction(['notifications'], 'readwrite') const store = transaction.objectStore('notifications') // Vérifier si la notification existe déjà const index = store.index('eventId') const existing = await executeTransactionOperation(index, (idx) => idx.get(eventId)).catch(() => null) if (existing) { // Notification déjà existante self.postMessage({ type: 'CREATE_NOTIFICATION_SUCCESS', data: { eventId, taskId, alreadyExists: true }, }) return } const notification = { id: `${type}_${objectType}_${objectId}_${eventId}_${Date.now()}`, type, objectType, objectId, eventId, timestamp: Date.now(), read: false, data: notificationData, title: notificationData?.title, message: notificationData?.message, articleId: notificationData?.articleId, articleTitle: notificationData?.articleTitle, amount: notificationData?.amount, fromPubkey: notificationData?.fromPubkey, } await executeTransactionOperation(store, (s) => s.add(notification)) self.postMessage({ type: 'CREATE_NOTIFICATION_SUCCESS', data: { eventId, taskId }, }) } catch (error) { throw new Error(`Failed to create notification: ${error.message}`) } } /** * Handle log publication request */ async function handleLogPublication(data) { const { eventId, relayUrl, success, error, objectType, objectId } = data try { const db = await openPublishLogDB() const transaction = db.transaction(['publications'], 'readwrite') const store = transaction.objectStore('publications') const entry = { id: `${eventId}_${relayUrl}_${Date.now()}`, eventId, relayUrl, success, error, timestamp: Date.now(), objectType, objectId, } await executeTransactionOperation(store, (s) => s.add(entry)) // Pas de réponse pour les logs (fire and forget) } catch (error) { // Don't throw for logs, just log the error console.error('[WriteWorker] Error logging publication:', error) } } /** * Generic helper to open IndexedDB database */ function openIndexedDB(dbName, version, upgradeHandler) { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version) request.onerror = () => reject(request.error) request.onsuccess = () => resolve(request.result) request.onupgradeneeded = (event) => { const db = event.target.result if (upgradeHandler) { upgradeHandler(db, event) } } }) } /** * Open IndexedDB for object type */ function openDB(objectType) { const dbName = `nostr_${objectType}_cache` const version = DB_VERSIONS[objectType] ?? 1 return openIndexedDB(dbName, version, (db) => { if (!db.objectStoreNames.contains('objects')) { const store = db.createObjectStore('objects', { keyPath: 'id' }) store.createIndex('hash', 'hash', { unique: false }) store.createIndex('index', 'index', { unique: false }) store.createIndex('published', 'published', { unique: false }) } else { // Migration : ajouter l'index published si nécessaire const transaction = db.transaction(['objects'], 'readwrite') const store = transaction.objectStore('objects') if (!store.indexNames.contains('published')) { store.createIndex('published', 'published', { unique: false }) } } }) } /** * Open IndexedDB for notifications */ function openNotificationDB() { return openIndexedDB('nostr_notifications', 1, (db) => { if (!db.objectStoreNames.contains('notifications')) { const store = db.createObjectStore('notifications', { keyPath: 'id' }) store.createIndex('type', 'type', { unique: false }) store.createIndex('objectId', 'objectId', { unique: false }) store.createIndex('eventId', 'eventId', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('read', 'read', { unique: false }) store.createIndex('objectType', 'objectType', { unique: false }) } }) } /** * Open IndexedDB for publish log */ function openPublishLogDB() { return openIndexedDB('nostr_publish_log', 1, (db) => { if (!db.objectStoreNames.contains('publications')) { const store = db.createObjectStore('publications', { keyPath: 'id', autoIncrement: true }) store.createIndex('eventId', 'eventId', { unique: false }) store.createIndex('relayUrl', 'relayUrl', { unique: false }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('success', 'success', { unique: false }) } }) } /** * Helper to execute a transaction operation */ function executeTransactionOperation(store, operation) { return new Promise((resolve, reject) => { const request = operation(store) request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } // Notify main thread that worker is ready self.postMessage({ type: 'WORKER_READY' }) console.log('[WriteWorker] Worker loaded')