feat: enhance website icon loading logic; implement error handling and timeout management

This commit is contained in:
shuaiplus
2026-05-09 23:46:33 +08:00
parent 7afb496eb0
commit 4e62c90700
4 changed files with 99 additions and 15 deletions
+89 -4
View File
@@ -3,9 +3,17 @@ type WebsiteIconStatus = 'idle' | 'loading' | 'loaded' | 'error';
interface WebsiteIconRecord {
status: WebsiteIconStatus;
imageUrl: string | null;
errorAt: number;
loadStartedAt: number;
loadToken: number;
loader: HTMLImageElement | null;
timeoutId: ReturnType<typeof setTimeout> | null;
listeners: Set<(status: WebsiteIconStatus) => void>;
}
const WEBSITE_ICON_ERROR_TTL_MS = 5 * 60 * 1000;
const WEBSITE_ICON_LOAD_TIMEOUT_MS = 15 * 1000;
const iconRecords = new Map<string, WebsiteIconRecord>();
function ensureRecord(host: string): WebsiteIconRecord {
@@ -14,6 +22,11 @@ function ensureRecord(host: string): WebsiteIconRecord {
record = {
status: 'idle',
imageUrl: null,
errorAt: 0,
loadStartedAt: 0,
loadToken: 0,
loader: null,
timeoutId: null,
listeners: new Set(),
};
iconRecords.set(host, record);
@@ -21,6 +34,29 @@ function ensureRecord(host: string): WebsiteIconRecord {
return record;
}
function clearLoadTimer(record: WebsiteIconRecord): void {
if (record.timeoutId) {
clearTimeout(record.timeoutId);
record.timeoutId = null;
}
}
function expireRecordIfNeeded(record: WebsiteIconRecord): void {
const now = Date.now();
if (record.status === 'error' && record.errorAt && now - record.errorAt >= WEBSITE_ICON_ERROR_TTL_MS) {
record.status = 'idle';
record.errorAt = 0;
record.imageUrl = null;
}
if (record.status === 'loading' && record.loadStartedAt && now - record.loadStartedAt >= WEBSITE_ICON_LOAD_TIMEOUT_MS) {
clearLoadTimer(record);
record.status = 'error';
record.errorAt = now;
record.imageUrl = null;
record.loader = null;
}
}
function notifyRecord(host: string, status: WebsiteIconStatus): void {
const record = ensureRecord(host);
record.status = status;
@@ -31,12 +67,16 @@ function notifyRecord(host: string, status: WebsiteIconStatus): void {
export function getWebsiteIconStatus(host: string): WebsiteIconStatus {
if (!host) return 'idle';
return ensureRecord(host).status;
const record = ensureRecord(host);
expireRecordIfNeeded(record);
return record.status;
}
export function getWebsiteIconImageUrl(host: string): string {
if (!host) return '';
return ensureRecord(host).imageUrl || '';
const record = ensureRecord(host);
expireRecordIfNeeded(record);
return record.imageUrl || '';
}
export function subscribeWebsiteIconStatus(host: string, listener: (status: WebsiteIconStatus) => void): () => void {
@@ -48,27 +88,72 @@ export function subscribeWebsiteIconStatus(host: string, listener: (status: Webs
};
}
export function markWebsiteIconLoaded(host: string, imageUrl?: string): void {
function markWebsiteIconLoaded(host: string, imageUrl?: string): void {
if (!host) return;
const record = ensureRecord(host);
clearLoadTimer(record);
if (imageUrl) {
record.imageUrl = imageUrl;
}
record.errorAt = 0;
record.loadStartedAt = 0;
record.loader = null;
notifyRecord(host, 'loaded');
}
export function markWebsiteIconErrored(host: string): void {
function markWebsiteIconErrored(host: string): void {
if (!host) return;
const record = ensureRecord(host);
clearLoadTimer(record);
record.imageUrl = null;
record.errorAt = Date.now();
record.loadStartedAt = 0;
record.loader = null;
notifyRecord(host, 'error');
}
export function beginWebsiteIconLoad(host: string, src: string): boolean {
if (!host || !src) return false;
const record = ensureRecord(host);
expireRecordIfNeeded(record);
if (record.status !== 'idle') return false;
if (typeof Image !== 'function') {
markWebsiteIconErrored(host);
return false;
}
const token = record.loadToken + 1;
const loader = new Image();
record.loadToken = token;
record.loader = loader;
record.imageUrl = src;
record.errorAt = 0;
record.loadStartedAt = Date.now();
notifyRecord(host, 'loading');
record.timeoutId = setTimeout(() => {
const current = ensureRecord(host);
if (current.loadToken !== token || current.status !== 'loading') return;
current.imageUrl = null;
current.errorAt = Date.now();
current.loadStartedAt = 0;
current.loader = null;
current.timeoutId = null;
notifyRecord(host, 'error');
}, WEBSITE_ICON_LOAD_TIMEOUT_MS);
loader.onload = () => {
const current = ensureRecord(host);
if (current.loadToken !== token) return;
markWebsiteIconLoaded(host, src);
};
loader.onerror = () => {
const current = ensureRecord(host);
if (current.loadToken !== token) return;
markWebsiteIconErrored(host);
};
loader.src = src;
return true;
}