mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
fix PR comments
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Skeleton from 'primevue/skeleton'
|
import Skeleton from 'primevue/skeleton'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||||
import { useMediaCache } from '@/services/mediaCacheService'
|
import { useMediaCache } from '@/services/mediaCacheService'
|
||||||
@@ -41,7 +41,7 @@ const {
|
|||||||
alt = '',
|
alt = '',
|
||||||
imageClass = '',
|
imageClass = '',
|
||||||
imageStyle,
|
imageStyle,
|
||||||
rootMargin = '50px'
|
rootMargin = '300px'
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
src: string
|
src: string
|
||||||
alt?: string
|
alt?: string
|
||||||
@@ -57,7 +57,7 @@ const isImageLoaded = ref(false)
|
|||||||
const hasError = ref(false)
|
const hasError = ref(false)
|
||||||
const cachedSrc = ref<string | undefined>(undefined)
|
const cachedSrc = ref<string | undefined>(undefined)
|
||||||
|
|
||||||
const { getCachedMedia } = useMediaCache()
|
const { getCachedMedia, acquireUrl, releaseUrl } = useMediaCache()
|
||||||
|
|
||||||
// Use intersection observer to detect when the image container comes into view
|
// Use intersection observer to detect when the image container comes into view
|
||||||
useIntersectionObserver(
|
useIntersectionObserver(
|
||||||
@@ -75,7 +75,6 @@ useIntersectionObserver(
|
|||||||
// Only start loading the image when it's in view
|
// Only start loading the image when it's in view
|
||||||
const shouldLoad = computed(() => isIntersecting.value)
|
const shouldLoad = computed(() => isIntersecting.value)
|
||||||
|
|
||||||
// Watch for when we should load and handle caching
|
|
||||||
watch(
|
watch(
|
||||||
shouldLoad,
|
shouldLoad,
|
||||||
async (shouldLoad) => {
|
async (shouldLoad) => {
|
||||||
@@ -85,16 +84,19 @@ watch(
|
|||||||
if (cachedMedia.error) {
|
if (cachedMedia.error) {
|
||||||
hasError.value = true
|
hasError.value = true
|
||||||
} else if (cachedMedia.objectUrl) {
|
} else if (cachedMedia.objectUrl) {
|
||||||
cachedSrc.value = cachedMedia.objectUrl
|
const acquiredUrl = acquireUrl(src)
|
||||||
|
cachedSrc.value = acquiredUrl || cachedMedia.objectUrl
|
||||||
} else {
|
} else {
|
||||||
cachedSrc.value = src
|
cachedSrc.value = src
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load cached media:', error)
|
console.warn('Failed to load cached media:', error)
|
||||||
// Fallback to original src
|
|
||||||
cachedSrc.value = src
|
cachedSrc.value = src
|
||||||
}
|
}
|
||||||
} else if (!shouldLoad) {
|
} else if (!shouldLoad) {
|
||||||
|
if (cachedSrc.value?.startsWith('blob:')) {
|
||||||
|
releaseUrl(src)
|
||||||
|
}
|
||||||
// Hide image when out of view
|
// Hide image when out of view
|
||||||
isImageLoaded.value = false
|
isImageLoaded.value = false
|
||||||
cachedSrc.value = undefined
|
cachedSrc.value = undefined
|
||||||
@@ -113,4 +115,10 @@ const onImageError = () => {
|
|||||||
hasError.value = true
|
hasError.value = true
|
||||||
isImageLoaded.value = false
|
isImageLoaded.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (cachedSrc.value?.startsWith('blob:')) {
|
||||||
|
releaseUrl(src)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { shallowRef } from 'vue'
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
export interface CachedMedia {
|
export interface CachedMedia {
|
||||||
src: string
|
src: string
|
||||||
@@ -16,10 +16,11 @@ export interface MediaCacheOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MediaCacheService {
|
class MediaCacheService {
|
||||||
public cache = shallowRef(new Map<string, CachedMedia>())
|
public cache = reactive(new Map<string, CachedMedia>())
|
||||||
private readonly maxSize: number
|
private readonly maxSize: number
|
||||||
private readonly maxAge: number
|
private readonly maxAge: number
|
||||||
private cleanupInterval: number | null = null
|
private cleanupInterval: number | null = null
|
||||||
|
private urlRefCount = new Map<string, number>()
|
||||||
|
|
||||||
constructor(options: MediaCacheOptions = {}) {
|
constructor(options: MediaCacheOptions = {}) {
|
||||||
this.maxSize = options.maxSize ?? 100
|
this.maxSize = options.maxSize ?? 100
|
||||||
@@ -41,49 +42,58 @@ class MediaCacheService {
|
|||||||
|
|
||||||
private cleanup() {
|
private cleanup() {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const cacheMap = this.cache.value
|
|
||||||
const keysToDelete: string[] = []
|
const keysToDelete: string[] = []
|
||||||
|
|
||||||
// Find expired entries
|
// Find expired entries
|
||||||
for (const [key, entry] of Array.from(cacheMap.entries())) {
|
for (const [key, entry] of Array.from(this.cache.entries())) {
|
||||||
if (now - entry.lastAccessed > this.maxAge) {
|
if (now - entry.lastAccessed > this.maxAge) {
|
||||||
keysToDelete.push(key)
|
// Only revoke object URL if no components are using it
|
||||||
// Revoke object URL to free memory
|
|
||||||
if (entry.objectUrl) {
|
if (entry.objectUrl) {
|
||||||
URL.revokeObjectURL(entry.objectUrl)
|
const refCount = this.urlRefCount.get(entry.objectUrl) || 0
|
||||||
|
if (refCount === 0) {
|
||||||
|
URL.revokeObjectURL(entry.objectUrl)
|
||||||
|
this.urlRefCount.delete(entry.objectUrl)
|
||||||
|
keysToDelete.push(key)
|
||||||
|
}
|
||||||
|
// Don't delete cache entry if URL is still in use
|
||||||
|
} else {
|
||||||
|
keysToDelete.push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove expired entries
|
// Remove expired entries
|
||||||
if (keysToDelete.length > 0) {
|
keysToDelete.forEach((key) => this.cache.delete(key))
|
||||||
const newCache = new Map(cacheMap)
|
|
||||||
keysToDelete.forEach((key) => newCache.delete(key))
|
|
||||||
this.cache.value = newCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still over size limit, remove oldest entries
|
// If still over size limit, remove oldest entries that aren't in use
|
||||||
if (cacheMap.size > this.maxSize) {
|
if (this.cache.size > this.maxSize) {
|
||||||
const entries = Array.from(cacheMap.entries())
|
const entries = Array.from(this.cache.entries())
|
||||||
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
|
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
|
||||||
|
|
||||||
const toRemove = entries.slice(0, cacheMap.size - this.maxSize)
|
let removedCount = 0
|
||||||
const newCache = new Map(cacheMap)
|
const targetRemoveCount = this.cache.size - this.maxSize
|
||||||
|
|
||||||
|
for (const [key, entry] of entries) {
|
||||||
|
if (removedCount >= targetRemoveCount) break
|
||||||
|
|
||||||
toRemove.forEach(([key, entry]) => {
|
|
||||||
if (entry.objectUrl) {
|
if (entry.objectUrl) {
|
||||||
URL.revokeObjectURL(entry.objectUrl)
|
const refCount = this.urlRefCount.get(entry.objectUrl) || 0
|
||||||
|
if (refCount === 0) {
|
||||||
|
URL.revokeObjectURL(entry.objectUrl)
|
||||||
|
this.urlRefCount.delete(entry.objectUrl)
|
||||||
|
this.cache.delete(key)
|
||||||
|
removedCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.cache.delete(key)
|
||||||
|
removedCount++
|
||||||
}
|
}
|
||||||
newCache.delete(key)
|
}
|
||||||
})
|
|
||||||
|
|
||||||
this.cache.value = newCache
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCachedMedia(src: string): Promise<CachedMedia> {
|
async getCachedMedia(src: string): Promise<CachedMedia> {
|
||||||
const cacheMap = this.cache.value
|
let entry = this.cache.get(src)
|
||||||
let entry = cacheMap.get(src)
|
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
// Update last accessed time
|
// Update last accessed time
|
||||||
@@ -99,9 +109,7 @@ class MediaCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update cache with loading entry
|
// Update cache with loading entry
|
||||||
const newCache = new Map(cacheMap)
|
this.cache.set(src, entry)
|
||||||
newCache.set(src, entry)
|
|
||||||
this.cache.value = newCache
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the media
|
// Fetch the media
|
||||||
@@ -122,10 +130,7 @@ class MediaCacheService {
|
|||||||
lastAccessed: Date.now()
|
lastAccessed: Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalCache = new Map(this.cache.value)
|
this.cache.set(src, updatedEntry)
|
||||||
finalCache.set(src, updatedEntry)
|
|
||||||
this.cache.value = finalCache
|
|
||||||
|
|
||||||
return updatedEntry
|
return updatedEntry
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to cache media:', src, error)
|
console.warn('Failed to cache media:', src, error)
|
||||||
@@ -138,44 +143,45 @@ class MediaCacheService {
|
|||||||
lastAccessed: Date.now()
|
lastAccessed: Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorCache = new Map(this.cache.value)
|
this.cache.set(src, errorEntry)
|
||||||
errorCache.set(src, errorEntry)
|
|
||||||
this.cache.value = errorCache
|
|
||||||
|
|
||||||
return errorEntry
|
return errorEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheStats() {
|
acquireUrl(src: string): string | undefined {
|
||||||
const cacheMap = this.cache.value
|
const entry = this.cache.get(src)
|
||||||
return {
|
if (entry?.objectUrl) {
|
||||||
size: cacheMap.size,
|
const currentCount = this.urlRefCount.get(entry.objectUrl) || 0
|
||||||
maxSize: this.maxSize,
|
this.urlRefCount.set(entry.objectUrl, currentCount + 1)
|
||||||
entries: Array.from(cacheMap.keys())
|
return entry.objectUrl
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseUrl(src: string): void {
|
||||||
|
const entry = this.cache.get(src)
|
||||||
|
if (entry?.objectUrl) {
|
||||||
|
const count = (this.urlRefCount.get(entry.objectUrl) || 1) - 1
|
||||||
|
if (count <= 0) {
|
||||||
|
URL.revokeObjectURL(entry.objectUrl)
|
||||||
|
this.urlRefCount.delete(entry.objectUrl)
|
||||||
|
// Remove from cache as well
|
||||||
|
this.cache.delete(src)
|
||||||
|
} else {
|
||||||
|
this.urlRefCount.set(entry.objectUrl, count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache() {
|
clearCache() {
|
||||||
const cacheMap = this.cache.value
|
|
||||||
// Revoke all object URLs
|
// Revoke all object URLs
|
||||||
for (const entry of Array.from(cacheMap.values())) {
|
for (const entry of Array.from(this.cache.values())) {
|
||||||
if (entry.objectUrl) {
|
if (entry.objectUrl) {
|
||||||
URL.revokeObjectURL(entry.objectUrl)
|
URL.revokeObjectURL(entry.objectUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cache.value = new Map()
|
this.cache.clear()
|
||||||
}
|
this.urlRefCount.clear()
|
||||||
|
|
||||||
preloadMedia(urls: string[]) {
|
|
||||||
// Preload media in the background without blocking
|
|
||||||
urls.forEach((url) => {
|
|
||||||
if (!this.cache.value.has(url)) {
|
|
||||||
// Don't await - fire and forget
|
|
||||||
this.getCachedMedia(url).catch(() => {
|
|
||||||
// Ignore preload errors
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -188,7 +194,7 @@ class MediaCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global instance
|
// Global instance
|
||||||
let mediaCacheInstance: MediaCacheService | null = null
|
export let mediaCacheInstance: MediaCacheService | null = null
|
||||||
|
|
||||||
export function useMediaCache(options?: MediaCacheOptions) {
|
export function useMediaCache(options?: MediaCacheOptions) {
|
||||||
if (!mediaCacheInstance) {
|
if (!mediaCacheInstance) {
|
||||||
@@ -197,16 +203,15 @@ export function useMediaCache(options?: MediaCacheOptions) {
|
|||||||
|
|
||||||
const getCachedMedia = (src: string) =>
|
const getCachedMedia = (src: string) =>
|
||||||
mediaCacheInstance!.getCachedMedia(src)
|
mediaCacheInstance!.getCachedMedia(src)
|
||||||
const getCacheStats = () => mediaCacheInstance!.getCacheStats()
|
|
||||||
const clearCache = () => mediaCacheInstance!.clearCache()
|
const clearCache = () => mediaCacheInstance!.clearCache()
|
||||||
const preloadMedia = (urls: string[]) =>
|
const acquireUrl = (src: string) => mediaCacheInstance!.acquireUrl(src)
|
||||||
mediaCacheInstance!.preloadMedia(urls)
|
const releaseUrl = (src: string) => mediaCacheInstance!.releaseUrl(src)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCachedMedia,
|
getCachedMedia,
|
||||||
getCacheStats,
|
|
||||||
clearCache,
|
clearCache,
|
||||||
preloadMedia,
|
acquireUrl,
|
||||||
|
releaseUrl,
|
||||||
cache: mediaCacheInstance.cache
|
cache: mediaCacheInstance.cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
tests-ui/tests/services/mediaCacheService.test.ts
Normal file
35
tests-ui/tests/services/mediaCacheService.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { useMediaCache } from '../../../src/services/mediaCacheService'
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = vi.fn()
|
||||||
|
global.URL = {
|
||||||
|
createObjectURL: vi.fn(() => 'blob:mock-url'),
|
||||||
|
revokeObjectURL: vi.fn()
|
||||||
|
} as any
|
||||||
|
|
||||||
|
describe('mediaCacheService', () => {
|
||||||
|
describe('URL reference counting', () => {
|
||||||
|
it('should handle URL acquisition for non-existent cache entry', () => {
|
||||||
|
const { acquireUrl } = useMediaCache()
|
||||||
|
|
||||||
|
const url = acquireUrl('non-existent.jpg')
|
||||||
|
expect(url).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle URL release for non-existent cache entry', () => {
|
||||||
|
const { releaseUrl } = useMediaCache()
|
||||||
|
|
||||||
|
// Should not throw error
|
||||||
|
expect(() => releaseUrl('non-existent.jpg')).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should provide acquireUrl and releaseUrl methods', () => {
|
||||||
|
const cache = useMediaCache()
|
||||||
|
|
||||||
|
expect(typeof cache.acquireUrl).toBe('function')
|
||||||
|
expect(typeof cache.releaseUrl).toBe('function')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user