12 changed files with 463 additions and 16 deletions
@ -0,0 +1,48 @@ |
|||||||
|
import { describe, expect, it } from 'vitest' |
||||||
|
import { |
||||||
|
approxLibraryIndexEventBytes, |
||||||
|
getLibraryIndexCacheBudget, |
||||||
|
LIBRARY_INDEX_CACHE_DEFAULTS |
||||||
|
} from '@/lib/library-index-cache-config' |
||||||
|
import { ExtendedKind } from '@/constants' |
||||||
|
import type { Event } from 'nostr-tools' |
||||||
|
|
||||||
|
function sampleIndexEvent(tagCount: number): Event { |
||||||
|
const tags: string[][] = [ |
||||||
|
['d', 'sample-book'], |
||||||
|
['title', 'Sample Book'] |
||||||
|
] |
||||||
|
for (let i = 0; i < tagCount; i++) { |
||||||
|
tags.push(['a', `30041:${'a'.repeat(64)}:chapter-${i}`, 'wss://relay.example', 'b'.repeat(64)]) |
||||||
|
} |
||||||
|
return { |
||||||
|
id: 'c'.repeat(64), |
||||||
|
kind: ExtendedKind.PUBLICATION, |
||||||
|
pubkey: 'a'.repeat(64), |
||||||
|
created_at: 1_700_000_000, |
||||||
|
content: '', |
||||||
|
tags, |
||||||
|
sig: 'd'.repeat(128) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
describe('library-index-cache-config', () => { |
||||||
|
it('approxLibraryIndexEventBytes returns positive size', () => { |
||||||
|
const bytes = approxLibraryIndexEventBytes(sampleIndexEvent(3)) |
||||||
|
expect(bytes).toBeGreaterThan(100) |
||||||
|
}) |
||||||
|
|
||||||
|
it('getLibraryIndexCacheBudget returns platform defaults', () => { |
||||||
|
const budget = getLibraryIndexCacheBudget() |
||||||
|
expect(budget.maxEntries).toBeGreaterThanOrEqual(LIBRARY_INDEX_CACHE_DEFAULTS.maxEntriesMobile) |
||||||
|
expect(budget.maxBytes).toBeGreaterThanOrEqual(LIBRARY_INDEX_CACHE_DEFAULTS.maxMbMobile * 1024 * 1024) |
||||||
|
}) |
||||||
|
|
||||||
|
it('5000 mercury-sized indexes land near documented desktop budget', () => { |
||||||
|
const avgMercuryBytes = 14_131 |
||||||
|
const est5000Mb = (avgMercuryBytes * LIBRARY_INDEX_CACHE_DEFAULTS.maxEntriesDesktop) / (1024 * 1024) |
||||||
|
expect(est5000Mb).toBeGreaterThan(50) |
||||||
|
expect(est5000Mb).toBeLessThan(120) |
||||||
|
expect(LIBRARY_INDEX_CACHE_DEFAULTS.maxMbDesktop).toBeGreaterThanOrEqual(Math.ceil(est5000Mb)) |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
import { isImwaldElectron, isMobileBrowserProfile } from '@/lib/client-platform' |
||||||
|
import type { Event } from 'nostr-tools' |
||||||
|
|
||||||
|
/** Platform caps for the dedicated Library kind-30040 index LRU store (separate from EVENT_ARCHIVE). */ |
||||||
|
export const LIBRARY_INDEX_CACHE_DEFAULTS = { |
||||||
|
maxEntriesMobile: 400, |
||||||
|
maxEntriesDesktop: 5000, |
||||||
|
maxEntriesElectron: 5000, |
||||||
|
maxMbMobile: 40, |
||||||
|
maxMbDesktop: 96, |
||||||
|
maxMbElectron: 128 |
||||||
|
} as const |
||||||
|
|
||||||
|
export function approxLibraryIndexEventBytes(ev: Event): number { |
||||||
|
try { |
||||||
|
return new Blob([JSON.stringify(ev)]).size |
||||||
|
} catch { |
||||||
|
return 2048 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function getLibraryIndexCacheBudget(): { maxEntries: number; maxBytes: number } { |
||||||
|
if (isImwaldElectron()) { |
||||||
|
return { |
||||||
|
maxEntries: LIBRARY_INDEX_CACHE_DEFAULTS.maxEntriesElectron, |
||||||
|
maxBytes: LIBRARY_INDEX_CACHE_DEFAULTS.maxMbElectron * 1024 * 1024 |
||||||
|
} |
||||||
|
} |
||||||
|
if (isMobileBrowserProfile()) { |
||||||
|
return { |
||||||
|
maxEntries: LIBRARY_INDEX_CACHE_DEFAULTS.maxEntriesMobile, |
||||||
|
maxBytes: LIBRARY_INDEX_CACHE_DEFAULTS.maxMbMobile * 1024 * 1024 |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
maxEntries: LIBRARY_INDEX_CACHE_DEFAULTS.maxEntriesDesktop, |
||||||
|
maxBytes: LIBRARY_INDEX_CACHE_DEFAULTS.maxMbDesktop * 1024 * 1024 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
import { ExtendedKind } from '@/constants' |
||||||
|
import { |
||||||
|
approxLibraryIndexEventBytes, |
||||||
|
getLibraryIndexCacheBudget |
||||||
|
} from '@/lib/library-index-cache-config' |
||||||
|
import logger from '@/lib/logger' |
||||||
|
import indexedDb from '@/services/indexed-db.service' |
||||||
|
import type { Event } from 'nostr-tools' |
||||||
|
|
||||||
|
export async function loadLibraryIndexCacheEvents(): Promise<Event[]> { |
||||||
|
try { |
||||||
|
return await indexedDb.getLibraryPublicationIndexCacheEvents() |
||||||
|
} catch (e) { |
||||||
|
if (import.meta.env.DEV) { |
||||||
|
logger.warn('[Library] index IDB read failed', { |
||||||
|
message: e instanceof Error ? e.message : String(e) |
||||||
|
}) |
||||||
|
} |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function persistLibraryIndexCacheEvents(events: Event[]): Promise<void> { |
||||||
|
const kind30040 = events.filter((ev) => ev.kind === ExtendedKind.PUBLICATION) |
||||||
|
if (kind30040.length === 0) return |
||||||
|
try { |
||||||
|
const budget = getLibraryIndexCacheBudget() |
||||||
|
await indexedDb.mergeLibraryPublicationIndexCacheEvents(kind30040, budget) |
||||||
|
} catch (e) { |
||||||
|
if (import.meta.env.DEV) { |
||||||
|
logger.warn('[Library] index IDB write failed', { |
||||||
|
message: e instanceof Error ? e.message : String(e) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function getLibraryIndexCacheFootprint(): Promise<{ count: number; bytes: number }> { |
||||||
|
try { |
||||||
|
return await indexedDb.getLibraryPublicationIndexCacheFootprint() |
||||||
|
} catch { |
||||||
|
return { count: 0, bytes: 0 } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function clearLibraryIndexIdbCache(): Promise<void> { |
||||||
|
await indexedDb.clearLibraryPublicationIndexCacheStore() |
||||||
|
} |
||||||
|
|
||||||
|
export { approxLibraryIndexEventBytes, getLibraryIndexCacheBudget } |
||||||
Loading…
Reference in new issue