1export interface ProjectFaviconRequest {
2 readonly cacheKey: string;
3 readonly faviconUrl: string;
4}
5
6interface ActiveFaviconRequests {
7 readonly urls: Map<string, number>;
8 currentUrl: string;
9}
10
11const MAX_LOADED_FAVICONS = 256;
12const activeFaviconRequests = new Map<string, ActiveFaviconRequests>();
13const loadedFaviconKeys = new Map<string, true>();
14
15export function createProjectFaviconRequest(
16 cacheKey: string,
17 faviconUrl: string,
18): ProjectFaviconRequest;
19export function createProjectFaviconRequest(
20 cacheKey: string | null,
21 faviconUrl: string | null,
22): ProjectFaviconRequest | null;
23export function createProjectFaviconRequest(cacheKey: string | null, faviconUrl: string | null) {
24 if (!cacheKey || !faviconUrl) return null;
25 return { cacheKey, faviconUrl };
26}
27
28export function beginProjectFaviconRequest(request: ProjectFaviconRequest) {
29 let activeRequests = activeFaviconRequests.get(request.cacheKey);
30 if (!activeRequests) {
31 activeRequests = { currentUrl: request.faviconUrl, urls: new Map() };
32 activeFaviconRequests.set(request.cacheKey, activeRequests);
33 }
34
35 const activeCount = activeRequests.urls.get(request.faviconUrl) ?? 0;
36 activeRequests.urls.delete(request.faviconUrl);
37 activeRequests.urls.set(request.faviconUrl, activeCount + 1);
38 activeRequests.currentUrl = request.faviconUrl;
39
40 let ended = false;
41 return () => {
42 if (ended) return;
43 ended = true;
44
45 const remainingCount = (activeRequests.urls.get(request.faviconUrl) ?? 1) - 1;
46 if (remainingCount > 0) {
47 activeRequests.urls.set(request.faviconUrl, remainingCount);
48 return;
49 }
50
51 activeRequests.urls.delete(request.faviconUrl);
52 if (activeRequests.urls.size === 0) {
53 if (activeFaviconRequests.get(request.cacheKey) === activeRequests) {
54 activeFaviconRequests.delete(request.cacheKey);
55 }
56 return;
57 }
58
59 if (activeRequests.currentUrl === request.faviconUrl) {
60 activeRequests.currentUrl = Array.from(activeRequests.urls.keys()).at(-1)!;
61 }
62 };
63}
64
65export function hasLoadedProjectFavicon(cacheKey: string | null) {
66 return cacheKey !== null && loadedFaviconKeys.has(cacheKey);
67}
68
69function isCurrentProjectFaviconRequest(request: ProjectFaviconRequest) {
70 return activeFaviconRequests.get(request.cacheKey)?.currentUrl === request.faviconUrl;
71}
72
73function rememberLoadedProjectFavicon(cacheKey: string) {
74 loadedFaviconKeys.delete(cacheKey);
75 loadedFaviconKeys.set(cacheKey, true);
76
77 if (loadedFaviconKeys.size > MAX_LOADED_FAVICONS) {
78 loadedFaviconKeys.delete(loadedFaviconKeys.keys().next().value!);
79 }
80}
81
82export function markProjectFaviconLoaded(request: ProjectFaviconRequest) {
83 if (!isCurrentProjectFaviconRequest(request)) return false;
84
85 rememberLoadedProjectFavicon(request.cacheKey);
86 return true;
87}
88
89export function markProjectFaviconFailed(request: ProjectFaviconRequest) {
90 if (!isCurrentProjectFaviconRequest(request)) return false;
91
92 loadedFaviconKeys.delete(request.cacheKey);
93 return true;
94}