485 lines
14 KiB
JavaScript
485 lines
14 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,
|
|
}
|
|
|
|
// 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, id)
|
|
break
|
|
case 'WRITE_MULTI_TABLE':
|
|
await handleWriteMultiTable(data, id)
|
|
break
|
|
default:
|
|
throw new Error(`Unknown message type: ${type}`)
|
|
}
|
|
}
|
|
|
|
// Listen for messages from main thread
|
|
self.addEventListener('message', (event) => {
|
|
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 new Promise((resolve, reject) => {
|
|
const request = store.get(finalId)
|
|
request.onsuccess = () => resolve(request.result)
|
|
request.onerror = () => reject(request.error)
|
|
}).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 new Promise((resolve, reject) => {
|
|
const request = store.put(object)
|
|
request.onsuccess = () => resolve()
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
|
|
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 new Promise((resolve, reject) => {
|
|
const request = store.get(id)
|
|
request.onsuccess = () => resolve(request.result)
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
|
|
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, taskId) {
|
|
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 new Promise((resolve, reject) => {
|
|
const request = store.get(finalId)
|
|
request.onsuccess = () => resolve(request.result)
|
|
request.onerror = () => reject(request.error)
|
|
}).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, taskId },
|
|
})
|
|
} 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 new Promise((resolve, reject) => {
|
|
const request = index.get(eventId)
|
|
request.onsuccess = () => resolve(request.result)
|
|
request.onerror = () => reject(request.error)
|
|
}).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 new Promise((resolve, reject) => {
|
|
const request = store.add(notification)
|
|
request.onsuccess = () => resolve()
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
|
|
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, taskId) {
|
|
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 new Promise((resolve, reject) => {
|
|
const request = store.add(entry)
|
|
request.onsuccess = () => resolve()
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open IndexedDB for object type
|
|
*/
|
|
function openDB(objectType) {
|
|
return new Promise((resolve, reject) => {
|
|
const dbName = `nostr_${objectType}_cache`
|
|
const version = DB_VERSIONS[objectType] ?? 1
|
|
|
|
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 (!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 = event.target.transaction
|
|
const store = transaction.objectStore('objects')
|
|
if (!store.indexNames.contains('published')) {
|
|
store.createIndex('published', 'published', { unique: false })
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Open IndexedDB for notifications
|
|
*/
|
|
function openNotificationDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('nostr_notifications', 1)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve(request.result)
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = event.target.result
|
|
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 new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('nostr_publish_log', 1)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve(request.result)
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = event.target.result
|
|
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 })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Notify main thread that worker is ready
|
|
self.postMessage({ type: 'WORKER_READY' })
|
|
|
|
console.log('[WriteWorker] Worker loaded')
|