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 */
9export interface ProjectFaviconRequest {
10 readonly cacheKey: string;
11 readonly faviconUrl: string;
12}
13
14interface ActiveFaviconRequests {
15 readonly urls: Map<string, number>;
16 currentUrl: string;
17}
18
19const MAX_LOADED_FAVICONS = 256;
20const activeFaviconRequests = new Map<string, ActiveFaviconRequests>();
21const loadedFaviconKeys = new Map<string, true>();
22
23export function createProjectFaviconRequest(
24 cacheKey: string,
25 faviconUrl: string,
26): ProjectFaviconRequest;
27export function createProjectFaviconRequest(
28 cacheKey: string | null,
29 faviconUrl: string | null,
30): ProjectFaviconRequest | null;
31export function createProjectFaviconRequest(cacheKey: string | null, faviconUrl: string | null) {
32 if (!cacheKey || !faviconUrl) return null;
33 return { cacheKey, faviconUrl };
34}
35
36export 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
73export function hasLoadedProjectFavicon(cacheKey: string | null) {
74 return cacheKey !== null && loadedFaviconKeys.has(cacheKey);
75}
76
77function isCurrentProjectFaviconRequest(request: ProjectFaviconRequest) {
78 return activeFaviconRequests.get(request.cacheKey)?.currentUrl === request.faviconUrl;
79}
80
81function 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
90export function markProjectFaviconLoaded(request: ProjectFaviconRequest) {
91 if (!isCurrentProjectFaviconRequest(request)) return false;
92
93 rememberLoadedProjectFavicon(request.cacheKey);
94 return true;
95}
96
97export function markProjectFaviconFailed(request: ProjectFaviconRequest) {
98 if (!isCurrentProjectFaviconRequest(request)) return false;
99
100 loadedFaviconKeys.delete(request.cacheKey);
101 return true;
102}