2026-01-08 23:04:56 +01:00

468 lines
13 KiB
JavaScript

/**
* 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,
}
const self = globalThis
const indexedDB = globalThis.indexedDB
const IDBKeyRange = globalThis.IDBKeyRange
// 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')