203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
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
|
|
};
|
|
})();
|