window.BrowserCache = (function () { var PREFIX = 'ying-cache:'; var DEBUG_KEY = PREFIX + 'debug'; var DEBUG = detectDebug(); function detectDebug() { try { if (typeof window === 'undefined' || !window.location) return false; var loc = window.location; var host = loc.hostname || ''; var protocol = loc.protocol || ''; var search = loc.search || ''; // Explicit URL override wins over everything. if (/(^|[?&])debug-cache=1(&|$)/.test(search)) return true; if (/(^|[?&])debug-cache=0(&|$)/.test(search)) return false; // Persisted toggle (set via setDebug) wins over auto-detection. try { var stored = localStorage.getItem(DEBUG_KEY); if (stored === '1') return true; if (stored === '0') return false; } catch (e) {} // Auto-enable on local dev origins. if (protocol === 'file:') return true; if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') return true; } catch (e) {} return false; } function setDebug(enabled) { DEBUG = !!enabled; try { localStorage.setItem(DEBUG_KEY, DEBUG ? '1' : '0'); } catch (e) {} return DEBUG; } function isDebug() { return DEBUG; } function isStorageAvailable() { try { var key = PREFIX + '__test__'; localStorage.setItem(key, '1'); localStorage.removeItem(key); return true; } catch (e) { return false; } } function buildKey(key) { return PREFIX + key; } function log(tagOrEvent, payload) { if (!DEBUG || typeof console === 'undefined' || !console.info) return; var label = tagOrEvent && tagOrEvent.charAt(0) === '[' ? tagOrEvent : '[BrowserCache][' + tagOrEvent + ']'; console.info(label, payload || {}); } function formatDuration(ms) { if (!ms || ms <= 0) return '0s'; var totalSeconds = Math.floor(ms / 1000); var hours = Math.floor(totalSeconds / 3600); var minutes = Math.floor((totalSeconds % 3600) / 60); var seconds = totalSeconds % 60; if (hours > 0) return hours + 'h ' + minutes + 'm'; if (minutes > 0) return minutes + 'm ' + seconds + 's'; return seconds + 's'; } function remove(key) { if (!isStorageAvailable()) return; try { localStorage.removeItem(buildKey(key)); log('remove', { key: buildKey(key) }); } catch (e) {} } function read(key, options) { options = options || {}; if (!isStorageAvailable()) { log('storage-unavailable', { key: buildKey(key) }); return null; } var fullKey = buildKey(key); try { var raw = localStorage.getItem(fullKey); if (!raw) { log('miss', { key: fullKey }); return null; } var payload = JSON.parse(raw); if (!payload || typeof payload !== 'object') { remove(key); log('invalid-payload', { key: fullKey }); return null; } if (payload.version !== options.version) { remove(key); log('version-mismatch', { key: fullKey, expected: options.version, actual: payload.version }); return null; } if (typeof payload.cachedAt !== 'number' || !('data' in payload)) { remove(key); log('invalid-shape', { key: fullKey }); return null; } if (typeof options.validate === 'function' && !options.validate(payload.data)) { remove(key); log('validate-failed', { key: fullKey }); return null; } var now = Date.now(); var age = now - payload.cachedAt; var softTtl = options.softTtl || 0; var hardTtl = options.hardTtl || softTtl; if (hardTtl > 0 && age > hardTtl) { remove(key); log('expired', { key: fullKey, age: formatDuration(age), hardTtl: formatDuration(hardTtl) }); return null; } var result = { key: fullKey, data: payload.data, cachedAt: payload.cachedAt, age: age, ageText: formatDuration(age), softTtl: softTtl, softTtlText: formatDuration(softTtl), hardTtl: hardTtl, hardTtlText: formatDuration(hardTtl), isFresh: softTtl <= 0 || age <= softTtl, isStale: softTtl > 0 && age > softTtl }; log(result.isFresh ? 'hit-fresh' : 'hit-stale', { key: fullKey, age: result.ageText, softTtl: result.softTtlText, hardTtl: result.hardTtlText }); return result; } catch (e) { remove(key); log('parse-failed', { key: fullKey, message: e && e.message ? e.message : e }); return null; } } function write(key, options, data) { options = options || {}; if (!isStorageAvailable()) { log('storage-unavailable', { key: buildKey(key) }); return; } var fullKey = buildKey(key); try { localStorage.setItem(fullKey, JSON.stringify({ version: options.version, cachedAt: Date.now(), data: data })); log('write', { key: fullKey, softTtl: formatDuration(options.softTtl || 0), hardTtl: formatDuration(options.hardTtl || options.softTtl || 0) }); } catch (e) { if (DEBUG && typeof console !== 'undefined' && console.warn) { console.warn('BrowserCache write failed:', e); } } } return { read: read, write: write, remove: remove, buildKey: buildKey, formatDuration: formatDuration, log: log, setDebug: setDebug, isDebug: isDebug }; })();