| 1 | /** |
| 2 | * Request bookkeeping behind `ProjectFavicon`: tracks the favicon URLs with |
| 3 | * mounted views per cache key, keeps one of them current, and remembers which |
| 4 | * keys have already loaded so superseded or already-shown loads settle without |
| 5 | * churn. This is not request coalescing — each mount still issues its own |
| 6 | * load — and it is not the cache; persistence lives in |
| 7 | * `projectFaviconDatabaseCache`. |
| 8 | */ |
| 9 | export interface ProjectFaviconRequest { |
| 10 | readonly cacheKey: string; |
| 11 | readonly faviconUrl: string; |
| 12 | } |
| 13 | |
| 14 | interface ActiveFaviconRequests { |
| 15 | readonly urls: Map<string, number>; |
| 16 | currentUrl: string; |
| 17 | } |
| 18 | |
| 19 | const MAX_LOADED_FAVICONS = 256; |
| 20 | const activeFaviconRequests = new Map<string, ActiveFaviconRequests>(); |
| 21 | const loadedFaviconKeys = new Map<string, true>(); |
| 22 | |
| 23 | export function createProjectFaviconRequest( |
| 24 | cacheKey: string, |
| 25 | faviconUrl: string, |
| 26 | ): ProjectFaviconRequest; |
| 27 | export function createProjectFaviconRequest( |
| 28 | cacheKey: string | null, |
| 29 | faviconUrl: string | null, |
| 30 | ): ProjectFaviconRequest | null; |
| 31 | export function createProjectFaviconRequest(cacheKey: string | null, faviconUrl: string | null) { |
| 32 | if (!cacheKey || !faviconUrl) return null; |
| 33 | return { cacheKey, faviconUrl }; |
| 34 | } |
| 35 | |
| 36 | export function beginProjectFaviconRequest(request: ProjectFaviconRequest) { |
| 37 | let activeRequests = activeFaviconRequests.get(request.cacheKey); |
| 38 | if (!activeRequests) { |
| 39 | activeRequests = { currentUrl: request.faviconUrl, urls: new Map() }; |
| 40 | activeFaviconRequests.set(request.cacheKey, activeRequests); |
| 41 | } |
| 42 | |
| 43 | const activeCount = activeRequests.urls.get(request.faviconUrl) ?? 0; |
| 44 | activeRequests.urls.delete(request.faviconUrl); |
| 45 | activeRequests.urls.set(request.faviconUrl, activeCount + 1); |
| 46 | activeRequests.currentUrl = request.faviconUrl; |
| 47 | |
| 48 | let ended = false; |
| 49 | return () => { |
| 50 | if (ended) return; |
| 51 | ended = true; |
| 52 | |
| 53 | const remainingCount = (activeRequests.urls.get(request.faviconUrl) ?? 1) - 1; |
| 54 | if (remainingCount > 0) { |
| 55 | activeRequests.urls.set(request.faviconUrl, remainingCount); |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | activeRequests.urls.delete(request.faviconUrl); |
| 60 | if (activeRequests.urls.size === 0) { |
| 61 | if (activeFaviconRequests.get(request.cacheKey) === activeRequests) { |
| 62 | activeFaviconRequests.delete(request.cacheKey); |
| 63 | } |
| 64 | return; |
| 65 | } |
| 66 | |
| 67 | if (activeRequests.currentUrl === request.faviconUrl) { |
| 68 | activeRequests.currentUrl = Array.from(activeRequests.urls.keys()).at(-1)!; |
| 69 | } |
| 70 | }; |
| 71 | } |
| 72 | |
| 73 | export function hasLoadedProjectFavicon(cacheKey: string | null) { |
| 74 | return cacheKey !== null && loadedFaviconKeys.has(cacheKey); |
| 75 | } |
| 76 | |
| 77 | function isCurrentProjectFaviconRequest(request: ProjectFaviconRequest) { |
| 78 | return activeFaviconRequests.get(request.cacheKey)?.currentUrl === request.faviconUrl; |
| 79 | } |
| 80 | |
| 81 | function rememberLoadedProjectFavicon(cacheKey: string) { |
| 82 | loadedFaviconKeys.delete(cacheKey); |
| 83 | loadedFaviconKeys.set(cacheKey, true); |
| 84 | |
| 85 | if (loadedFaviconKeys.size > MAX_LOADED_FAVICONS) { |
| 86 | loadedFaviconKeys.delete(loadedFaviconKeys.keys().next().value!); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | export function markProjectFaviconLoaded(request: ProjectFaviconRequest) { |
| 91 | if (!isCurrentProjectFaviconRequest(request)) return false; |
| 92 | |
| 93 | rememberLoadedProjectFavicon(request.cacheKey); |
| 94 | return true; |
| 95 | } |
| 96 | |
| 97 | export function markProjectFaviconFailed(request: ProjectFaviconRequest) { |
| 98 | if (!isCurrentProjectFaviconRequest(request)) return false; |
| 99 | |
| 100 | loadedFaviconKeys.delete(request.cacheKey); |
| 101 | return true; |
| 102 | } |