| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // IndexedDB cache for preview images - matches original implementation
- import { apiClient } from './apiClient'
- const PREVIEW_CACHE_DB_NAME = 'dune_weaver_previews'
- const PREVIEW_CACHE_DB_VERSION = 1
- const PREVIEW_CACHE_STORE_NAME = 'previews'
- const MAX_CACHE_SIZE_MB = 200
- const MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_MB * 1024 * 1024
- interface PreviewData {
- image_data: string
- first_coordinate: { x: number; y: number } | null
- last_coordinate: { x: number; y: number } | null
- error?: string
- }
- interface CacheEntry {
- pattern: string
- data: PreviewData
- size: number
- lastAccessed: number
- created: number
- }
- let previewCacheDB: IDBDatabase | null = null
- // In-memory cache for faster access during session
- const memoryCache = new Map<string, PreviewData>()
- const MAX_MEMORY_CACHE_SIZE = 100
- // Initialize IndexedDB
- export async function initPreviewCacheDB(): Promise<IDBDatabase> {
- if (previewCacheDB) return previewCacheDB
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(PREVIEW_CACHE_DB_NAME, PREVIEW_CACHE_DB_VERSION)
- request.onerror = () => {
- console.error('Failed to open preview cache database')
- reject(request.error)
- }
- request.onsuccess = () => {
- previewCacheDB = request.result
- console.debug('Preview cache database opened successfully')
- resolve(previewCacheDB)
- }
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result
- // Create object store for preview cache
- const store = db.createObjectStore(PREVIEW_CACHE_STORE_NAME, { keyPath: 'pattern' })
- store.createIndex('lastAccessed', 'lastAccessed', { unique: false })
- store.createIndex('size', 'size', { unique: false })
- console.debug('Preview cache database schema created')
- }
- })
- }
- // Get preview from cache (memory first, then IndexedDB)
- export async function getPreviewFromCache(pattern: string): Promise<PreviewData | null> {
- // Check memory cache first
- if (memoryCache.has(pattern)) {
- return memoryCache.get(pattern)!
- }
- // Check IndexedDB
- try {
- if (!previewCacheDB) await initPreviewCacheDB()
- const transaction = previewCacheDB!.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- return new Promise((resolve, reject) => {
- const request = store.get(pattern)
- request.onsuccess = () => {
- const result = request.result as CacheEntry | undefined
- if (result) {
- // Update last accessed time
- result.lastAccessed = Date.now()
- store.put(result)
- // Add to memory cache
- addToMemoryCache(pattern, result.data)
- resolve(result.data)
- } else {
- resolve(null)
- }
- }
- request.onerror = () => reject(request.error)
- })
- } catch (error) {
- console.warn(`Error getting preview from cache: ${error}`)
- return null
- }
- }
- // Add to memory cache with size limit
- function addToMemoryCache(pattern: string, data: PreviewData) {
- if (memoryCache.size >= MAX_MEMORY_CACHE_SIZE) {
- // Remove oldest entry (first key)
- const oldestKey = memoryCache.keys().next().value
- if (oldestKey) {
- memoryCache.delete(oldestKey)
- }
- }
- memoryCache.set(pattern, data)
- }
- // Save preview to IndexedDB cache with size management
- export async function savePreviewToCache(pattern: string, previewData: PreviewData): Promise<void> {
- try {
- if (!previewData || !previewData.image_data) {
- console.warn(`Invalid preview data for ${pattern}, skipping cache save`)
- return
- }
- if (!previewCacheDB) await initPreviewCacheDB()
- // Add to memory cache
- addToMemoryCache(pattern, previewData)
- // Calculate size from base64 data
- const size = Math.ceil((previewData.image_data.length * 3) / 4)
- // Check if we need to free up space
- await managePreviewCacheSize(size)
- const cacheEntry: CacheEntry = {
- pattern: pattern,
- data: previewData,
- size: size,
- lastAccessed: Date.now(),
- created: Date.now(),
- }
- const transaction = previewCacheDB!.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- return new Promise((resolve, reject) => {
- const request = store.put(cacheEntry)
- request.onsuccess = () => {
- console.debug(`Preview cached for ${pattern} (${(size / 1024).toFixed(1)}KB)`)
- resolve()
- }
- request.onerror = () => reject(request.error)
- })
- } catch (error) {
- console.warn(`Error saving preview to cache: ${error}`)
- }
- }
- // Get current cache size
- async function getPreviewCacheSize(): Promise<number> {
- try {
- if (!previewCacheDB) return 0
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- return new Promise((resolve, reject) => {
- const request = store.getAll()
- request.onsuccess = () => {
- const totalSize = request.result.reduce(
- (sum: number, entry: CacheEntry) => sum + (entry.size || 0),
- 0
- )
- resolve(totalSize)
- }
- request.onerror = () => reject(request.error)
- })
- } catch (error) {
- console.warn(`Error getting cache size: ${error}`)
- return 0
- }
- }
- // Manage cache size by removing least recently used items (LRU eviction)
- async function managePreviewCacheSize(newItemSize: number): Promise<void> {
- try {
- const currentSize = await getPreviewCacheSize()
- if (currentSize + newItemSize <= MAX_CACHE_SIZE_BYTES) {
- return // No cleanup needed
- }
- console.debug(
- `Cache size would exceed limit (${((currentSize + newItemSize) / 1024 / 1024).toFixed(1)}MB), cleaning up...`
- )
- const transaction = previewCacheDB!.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- const index = store.index('lastAccessed')
- // Get all entries sorted by last accessed (oldest first)
- const entries = await new Promise<CacheEntry[]>((resolve, reject) => {
- const request = index.getAll()
- request.onsuccess = () => resolve(request.result)
- request.onerror = () => reject(request.error)
- })
- // Sort by last accessed time (oldest first)
- entries.sort((a, b) => a.lastAccessed - b.lastAccessed)
- let freedSpace = 0
- const targetSpace = newItemSize + MAX_CACHE_SIZE_BYTES * 0.1 // Free 10% extra buffer
- for (const entry of entries) {
- if (freedSpace >= targetSpace) break
- await new Promise<void>((resolve, reject) => {
- const deleteRequest = store.delete(entry.pattern)
- deleteRequest.onsuccess = () => {
- freedSpace += entry.size
- // Also remove from memory cache
- memoryCache.delete(entry.pattern)
- console.debug(
- `Evicted cached preview for ${entry.pattern} (${(entry.size / 1024).toFixed(1)}KB)`
- )
- resolve()
- }
- deleteRequest.onerror = () => reject(deleteRequest.error)
- })
- }
- console.debug(`Freed ${(freedSpace / 1024 / 1024).toFixed(1)}MB from preview cache`)
- } catch (error) {
- console.warn(`Error managing cache size: ${error}`)
- }
- }
- // Clear a specific pattern from cache
- export async function clearPatternFromCache(pattern: string): Promise<void> {
- try {
- // Remove from memory cache
- memoryCache.delete(pattern)
- if (!previewCacheDB) await initPreviewCacheDB()
- const transaction = previewCacheDB!.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- await new Promise<void>((resolve, reject) => {
- const deleteRequest = store.delete(pattern)
- deleteRequest.onsuccess = () => {
- console.debug(`Cleared ${pattern} from cache`)
- resolve()
- }
- deleteRequest.onerror = () => reject(deleteRequest.error)
- })
- } catch (error) {
- console.warn(`Error clearing pattern from cache: ${error}`)
- }
- }
- // Get multiple previews from cache (batch operation)
- export async function getPreviewsFromCache(
- patterns: string[]
- ): Promise<Map<string, PreviewData>> {
- const results = new Map<string, PreviewData>()
- const uncachedPatterns: string[] = []
- // Check memory cache first
- for (const pattern of patterns) {
- if (memoryCache.has(pattern)) {
- results.set(pattern, memoryCache.get(pattern)!)
- } else {
- uncachedPatterns.push(pattern)
- }
- }
- // Check IndexedDB for remaining patterns
- if (uncachedPatterns.length > 0 && previewCacheDB) {
- try {
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite')
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME)
- await Promise.all(
- uncachedPatterns.map(
- (pattern) =>
- new Promise<void>((resolve) => {
- const request = store.get(pattern)
- request.onsuccess = () => {
- const result = request.result as CacheEntry | undefined
- if (result) {
- // Update last accessed time
- result.lastAccessed = Date.now()
- store.put(result)
- // Add to results and memory cache
- results.set(pattern, result.data)
- addToMemoryCache(pattern, result.data)
- }
- resolve()
- }
- request.onerror = () => resolve()
- })
- )
- )
- } catch (error) {
- console.warn(`Error batch getting from cache: ${error}`)
- }
- }
- return results
- }
- // Shared function to cache all previews - used by both BrowsePage and Layout modal
- export interface CacheAllProgress {
- completed: number
- total: number
- done: boolean
- }
- export async function cacheAllPreviews(
- onProgress: (progress: CacheAllProgress) => void
- ): Promise<{ success: boolean; cached: number }> {
- const BATCH_SIZE = 10
- try {
- await initPreviewCacheDB()
- // Fetch all patterns
- const patterns: { path: string }[] = await apiClient.get('/list_theta_rho_files_with_metadata')
- const allPaths = patterns.map((p) => p.path)
- // Check which patterns are already cached
- const cachedPreviews = await getPreviewsFromCache(allPaths)
- const uncachedPatterns = allPaths.filter((path) => !cachedPreviews.has(path))
- if (uncachedPatterns.length === 0) {
- onProgress({ completed: patterns.length, total: patterns.length, done: true })
- return { success: true, cached: 0 }
- }
- onProgress({ completed: 0, total: uncachedPatterns.length, done: false })
- const totalBatches = Math.ceil(uncachedPatterns.length / BATCH_SIZE)
- for (let i = 0; i < totalBatches; i++) {
- const batchStart = i * BATCH_SIZE
- const batchEnd = Math.min(batchStart + BATCH_SIZE, uncachedPatterns.length)
- const batchPatterns = uncachedPatterns.slice(batchStart, batchEnd)
- try {
- const results = await apiClient.post<Record<string, PreviewData>>('/preview_thr_batch', { file_names: batchPatterns })
- for (const [path, data] of Object.entries(results)) {
- if (data && !data.error) {
- await savePreviewToCache(path, data)
- }
- }
- } catch {
- // Continue even if batch fails
- }
- onProgress({ completed: batchEnd, total: uncachedPatterns.length, done: false })
- // Small delay between batches
- if (i + 1 < totalBatches) {
- await new Promise((resolve) => setTimeout(resolve, 100))
- }
- }
- onProgress({ completed: uncachedPatterns.length, total: uncachedPatterns.length, done: true })
- return { success: true, cached: uncachedPatterns.length }
- } catch (error) {
- console.error('Error caching previews:', error)
- return { success: false, cached: 0 }
- }
- }
|