Fix authors not loading from cache on startup

**Motivations:**
- Authors were not being loaded from cache, causing 'Aucun contenu trouvé' message even when authors exist
- useArticles only loaded articles from Nostr subscription, not authors from cache
- Authors should be loaded from cache first (cache-first architecture)

**Root causes:**
- useArticles hook only subscribed to articles from Nostr, not loading authors from cache
- No method to get all authors from cache
- Authors were only extracted from articles returned by subscription, which may not include author presentations

**Correctifs:**
- Added getAll method to objectCache to retrieve all objects of a type from cache
- Modified useArticles to load authors from cache on startup before subscribing to Nostr
- Authors are now loaded from cache and merged with articles from subscription
- totalSponsoring is calculated for each author when loading from cache

**Evolutions:**
- Authors are now available immediately from cache on page load
- Better user experience: no 'Aucun contenu trouvé' when authors exist in cache
- Cache-first architecture: authors loaded from cache before Nostr subscription

**Pages affectées:**
- lib/objectCache.ts
- hooks/useArticles.ts
This commit is contained in:
Nicolas Cantu 2026-01-06 15:33:02 +01:00
parent 52bd9492b7
commit 29cb20c614
4 changed files with 95 additions and 12 deletions

View File

@ -4,6 +4,8 @@ import type { Article } from '@/types/nostr'
import { applyFiltersAndSort } from '@/lib/articleFiltering'
import type { ArticleFilters } from '@/components/ArticleFilters'
import { t } from '@/lib/i18n'
import { objectCache } from '@/lib/objectCache'
import { getAuthorSponsoring } from '@/lib/sponsoring'
export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null): {
articles: Article[]
@ -21,6 +23,39 @@ export function useArticles(searchQuery: string = '', filters: ArticleFilters |
setLoading(true)
setError(null)
// Load authors from cache first
const loadAuthorsFromCache = async (): Promise<void> => {
try {
const cachedAuthors = await objectCache.getAll('author')
const authors = cachedAuthors as Article[]
// Calculate totalSponsoring for each author
const authorsWithSponsoring = await Promise.all(
authors.map(async (author) => {
if (author.isPresentation && author.pubkey) {
author.totalSponsoring = await getAuthorSponsoring(author.pubkey)
}
return author
})
)
if (authorsWithSponsoring.length > 0) {
setArticles((prev) => {
// Merge with existing articles, avoiding duplicates
const existingIds = new Set(prev.map((a) => a.id))
const newAuthors = authorsWithSponsoring.filter((a) => !existingIds.has(a.id))
const merged = [...prev, ...newAuthors].sort((a, b) => b.createdAt - a.createdAt)
hasArticlesRef.current = merged.length > 0
return merged
})
}
} catch (error) {
console.error('Error loading authors from cache:', error)
}
}
void loadAuthorsFromCache()
const unsubscribe = nostrService.subscribeToArticles(
(article) => {
setArticles((prev) => {

View File

@ -17,7 +17,12 @@ export async function buildPresentationEvent(
category: 'sciencefiction' | 'research' = 'sciencefiction',
version: number = 0,
index: number = 0
) {
): Promise<{
kind: 1
created_at: number
tags: string[][]
content: string
}> {
// Extract presentation and contentDescription from draft.content
// Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}"
const separator = '\n\n---\n\nDescription du contenu :\n'
@ -243,7 +248,7 @@ export async function fetchAuthorPresentationFromPool(
},
]
return new Promise((resolve) => {
return new Promise<import('@/types/nostr').AuthorPresentationArticle | null>((resolve) => {
let resolved = false
const relayUrl = getPrimaryRelaySync()
const { createSubscription } = require('@/types/nostr-tools-extended')
@ -251,7 +256,7 @@ export async function fetchAuthorPresentationFromPool(
const events: Event[] = []
const finalize = async (value: import('@/types/nostr').AuthorPresentationArticle | null) => {
const finalize = async (value: import('@/types/nostr').AuthorPresentationArticle | null): Promise<void> => {
if (resolved) {
return
}
@ -275,7 +280,7 @@ export async function fetchAuthorPresentationFromPool(
resolve(value)
}
sub.on('event', (event: Event) => {
sub.on('event', (event: Event): void => {
// Collect all events first
const tags = extractTagsFromEvent(event)
if (tags.type === 'author' && !tags.hidden) {
@ -283,7 +288,7 @@ export async function fetchAuthorPresentationFromPool(
}
})
sub.on('eose', async () => {
sub.on('eose', async (): Promise<void> => {
// Get the latest version from all collected events
const latestEvent = getLatestVersion(events)
if (latestEvent) {
@ -295,7 +300,7 @@ export async function fetchAuthorPresentationFromPool(
}
await finalize(null)
})
setTimeout(async () => {
setTimeout(async (): Promise<void> => {
// Get the latest version from all collected events
const latestEvent = getLatestVersion(events)
if (latestEvent) {

View File

@ -3,7 +3,14 @@ import { getPrimaryRelaySync } from './config'
import type { Event } from 'nostr-tools'
import { createSubscription } from '@/types/nostr-tools-extended'
export function createMessageVerificationFilters(messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string) {
export function createMessageVerificationFilters(messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string): Array<{
kinds: number[]
ids: string[]
authors: string[]
'#p': string[]
'#e': string[]
limit: number
}> {
return [
{
kinds: [4],
@ -42,11 +49,11 @@ export function setupMessageVerificationHandlers(
finalize: (value: boolean) => void,
isResolved: () => boolean
): void {
sub.on('event', (event: Event) => {
sub.on('event', (event: Event): void => {
handleMessageVerificationEvent(event, articleId, recipientPubkey, authorPubkey, finalize)
})
sub.on('eose', () => {
sub.on('eose', (): void => {
console.warn('Private message not found on relay after EOSE', {
messageEventId,
articleId,
@ -75,7 +82,7 @@ function createMessageVerificationSubscription(
authorPubkey: string,
recipientPubkey: string,
articleId: string
) {
): ReturnType<typeof createSubscription> {
const filters = createMessageVerificationFilters(messageEventId, authorPubkey, recipientPubkey, articleId)
const relayUrl = getPrimaryRelaySync()
return createSubscription(pool, [relayUrl], filters)
@ -88,10 +95,10 @@ function createVerificationPromise(
recipientPubkey: string,
authorPubkey: string
): Promise<boolean> {
return new Promise((resolve) => {
return new Promise<boolean>((resolve) => {
let resolved = false
const finalize = (value: boolean) => {
const finalize = (value: boolean): void => {
if (resolved) {
return
}

View File

@ -275,6 +275,42 @@ class ObjectCacheService {
}
}
/**
* Get all objects of a type from cache (non-hidden only)
*/
async getAll(objectType: ObjectType): Promise<unknown[]> {
try {
const db = await this.initDB(objectType)
const transaction = db.transaction(['objects'], 'readonly')
const store = transaction.objectStore('objects')
return new Promise((resolve, reject) => {
const request = store.openCursor()
const objects: unknown[] = []
request.onsuccess = (event: Event): void => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const obj = cursor.value as CachedObject
if (!obj.hidden) {
objects.push(obj.parsed)
}
cursor.continue()
} else {
resolve(objects)
}
}
request.onerror = (): void => {
reject(request.error)
}
})
} catch (error) {
console.error(`Error retrieving all ${objectType} objects from cache:`, error)
return []
}
}
/**
* Clear cache for an object type
*/