Update 14/05/2025

This commit is contained in:
2025-05-14 10:32:09 +07:00
parent e1980fe6a2
commit c75d9b72cf
6 changed files with 483 additions and 271 deletions

View File

@ -11,7 +11,7 @@
<title></title>
<link rel="icon" type="image/x-icon" href="@Url.AbsoluteContent("~/images/logo/icon.png")" />
<link rel="preload" as="script" href="@Url.AbsoluteContent("~/js/libs/js-core.js")">
<link rel="preload" as="style" href="@Url.AbsoluteContent("~/css/atg-lib/atg-core.css")" />
<link rel="preload" as="style" href="@Url.AbsoluteContent("~/css/atg-lib/atg-core.css")" onload="this.onload=null;" />
</head>
<body class="hp">
<section data-scrollbar class="main-scrollbar">

View File

@ -1,5 +1,4 @@
import AMenu from '/js/libs/js-AMenu.js'
import AOverScroll from '/js/ext_libs/js-AOverScroll.js'
import ALayout from '/js/libs/js-ALayout.js'
var asyncStyleSheets = [
@ -7,10 +6,11 @@ var asyncStyleSheets = [
'/css/atg-font/atg-admin-font.css'
];
window.app.loadCSS(asyncStyleSheets);
window.isLoad_Menu = false;
class AsyncLayout extends ALayout {
constructor() {
super();
this.isLoaded = false;
this.layMNav = `<div class="m-header">
<a app-nav href="${window.GetAbsoluteURL("/")}" class="c_logo d-f a-i-center c-a">
<img src="/images/logo/logo.jpg" />
@ -62,12 +62,22 @@ class AsyncLayout extends ALayout {
})
}
dispose() {
this.isLoaded = false;
var h = document.getElementById("header");
var f = document.getElementById("footer");
var h1 = document.getElementById("fHeader");
document.querySelector(".m-header").remove();
f.removeAll();
h.removeAll();
h1.remove();
window.app.removeSytemEventParent(window.app.lName);
window.app.removeCustomEventParent(window.app.lName);
super.dispose();
}
renderMenu() {
window.isLoad_Menu = true;
this.isLoaded = true;
this._createFHeader();
window.app.initNavs();
window.app.initNavs("Async");
var hHeader = document.getElementById("header").clientHeight;
var fHeader = document.getElementById("fHeader");
@ -83,10 +93,11 @@ class AsyncLayout extends ALayout {
fHeader.classList.remove("show");
}
});
this.addCustomEvent(idE0, window.app, "Asyc");
this.addCustomEvent(idE0, window.app, "Async");
var a1 = new AMenu("#header .nav-mainmenu", ".m-header", document.querySelectorAll(".m-navbar > .ico-menu"), true);
this.listAObject.add(a1);
var a2 = new AMenu("#fHeader .nav-mainmenu", null, null);
this.listAObject.add(a2);
const idE1 = window.app.on("redirect_page", (e) => {
a1.changeActive();
a2.changeActive();
@ -96,25 +107,9 @@ class AsyncLayout extends ALayout {
window.app.initNavApp(window.GetAbsoluteURL("/Search"));
}
const btnMHeader = document.getElementById("btnMHeader");
btnMHeader.addEventListener(this.eventName, f);
this.addSystemEvent(this.eventName, f, )
}
hideMenu() {
var h = document.getElementById("header");
var f = document.getElementById("footer");
var h1 = document.getElementById("fHeader");
h.classList.add("d-n");
f.classList.add("d-n");
h1.classList.add("d-n");
}
showMenu() {
var h = document.getElementById("header");
var f = document.getElementById("footer");
var h1 = document.getElementById("fHeader");
h.classList.remove("d-n");
f.classList.remove("d-n");
h1.classList.remove("d-n");
// btnMHeader.addEventListener(this.eventName, f);
this.addSystemEvent(this.eventName, btnMHeader, f);
}
}
window.ALayout.set("Async", new AsyncLayout());
window.AScript.set("asyncLayout", true);

View File

@ -1,13 +1,29 @@
import AMenu from '/js/libs/js-AMenu.js'
import AOverScroll from '/js/ext_libs/js-AOverScroll.js'
import ALayout from '/js/libs/js-ALayout.js'
var asyncStyleSheets = [
'https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap',
'/css/atg-font/atg-admin-font.css'
];
window.app.loadCSS(asyncStyleSheets);
window.isLoad_Menu = false;
const layMNav = `<div class="m-header">
window.Load_Menu = function () {
//var a1 = new AMenu("#header .nav-mainmenu", ".m-header", document.querySelectorAll(".m-navbar > .ico-menu"), true);
//var a2 = new AMenu("#fHeader .nav-mainmenu", null, null);
//console.log(document.getElementById("btnMHeader"));
//document.getElementById("btnMHeader").addEventListener("click", (e) => {
// window.app.initNavApp(window.GetAbsoluteURL("/Search"));
//});
}
class Async1Layout extends ALayout {
constructor() {
super();
this.isLoaded = false;
this.layMNav = `<div class="m-header">
<a app-nav href="${window.GetAbsoluteURL("/")}" class="c_logo d-f a-i-center c-a">
<img src="/images/logo/logo.jpg" />
<span class="d-f no-wrap ml-2"><b class="lg">VIN</b> <b class="lg lg1">FONT</b></span>
@ -19,8 +35,7 @@ const layMNav = `<div class="m-header">
</div>
</div>`;
const fHeader = `<div id="fHeader" class="f-header">
this.fHeader = `<div id="fHeader" class="f-header">
<div class="cfull">
<div class="c_search_header d-f a-i-center j-c-between">
<div class="c_filter c-6 c-s-4 c-l-2">
@ -53,45 +68,35 @@ const fHeader = `<div id="fHeader" class="f-header">
</div>
</div>`;
function createFHeader() {
document.body.insertAdjacentHTML("afterbegin", layMNav);
document.body.insertAdjacentHTML("afterbegin", fHeader);
}
_createFHeader() {
document.body.insertAdjacentHTML("afterbegin", this.layMNav);
document.body.insertAdjacentHTML("afterbegin", this.fHeader);
var h = document.querySelector("#fHeader .nav-mainmenu");
var h2 = document.querySelector(".m-header");
var he2 = h2.querySelector(".nav-mainmenu");
var r = document.querySelector("#header");
//r.querySelector(".nav-mainmenu").childNodes.forEach(el => {
// he2.appendChild(el.cloneNode(true));
// h.appendChild(el.cloneNode(true));
//})
}
window.Hide_Menu = function () {
dispose() {
this.isLoaded = false;
var h = document.getElementById("header");
var f = document.getElementById("footer");
var h1 = document.getElementById("fHeader");
h.classList.add("d-n");
f.classList.add("d-n");
h1.classList.add("d-n");
document.querySelector(".m-header").remove();
f.removeAll();
h.removeAll();
h1.remove();
window.app.removeSytemEventParent(window.app.lName);
window.app.removeCustomEventParent(window.app.lName);
super.dispose();
}
window.Show_Menu = function () {
var h = document.getElementById("header");
var f = document.getElementById("footer");
var h1 = document.getElementById("fHeader");
h.classList.remove("d-n");
f.classList.remove("d-n");
h1.classList.remove("d-n");
}
window.Load_Menu = function () {
window.isLoad_Menu = true;
window.app.on("redirect_page", (e) => {
a1.changeActive();
});
createFHeader();
window.app.initNavs();
renderMenu() {
this.isLoaded = true;
this._createFHeader();
window.app.initNavs("Async1");
var hHeader = document.getElementById("header").clientHeight;
var fHeader = document.getElementById("fHeader");
var plScr = new AOverScroll();
if (window.getOS() == "iOS") {
fHeader.classList.add("ios");
}
@ -104,13 +109,23 @@ window.Load_Menu = function () {
fHeader.classList.remove("show");
}
});
//this.addCustomEvent(idE0, window.app, "Async1");
//var a1 = new AMenu("#header .nav-mainmenu", ".m-header", document.querySelectorAll(".m-navbar > .ico-menu"), true);
//var a2 = new AMenu("#fHeader .nav-mainmenu", null, null);
//console.log(document.getElementById("btnMHeader"));
//document.getElementById("btnMHeader").addEventListener("click", (e) => {
// window.app.initNavApp(window.GetAbsoluteURL("/Search"));
//const idE1 = window.app.on("redirect_page", (e) => {
// a1.changeActive();
// a2.changeActive();
//});
//this.addCustomEvent(idE1, window.app, "Async");
//const f = function (e) {
// window.app.initNavApp(window.GetAbsoluteURL("/Search"));
//}
//const btnMHeader = document.getElementById("btnMHeader");
//// btnMHeader.addEventListener(this.eventName, f);
//this.addSystemEvent(this.eventName, btnMHeader, f);
}
}
window.ALayout.set("Async1", new Async1Layout());
window.AScript.set("asyncLayout1", true);

View File

@ -1,6 +1,6 @@
export default class ALayout extends window.AObject {
constructor() {
super();
}
renderMenu(){

View File

@ -70,8 +70,13 @@ export default class AMenu extends window.AObject {
if (this.isMDiffD) {
d2.bindDropDowns(this.navM.querySelectorAll(".nav-i.has-sub"));
}
this.dropdown = d2;
}
dispose() {
this.overlay.dispose();
this.dropdown.dispose();
super.dispose();
}
updateIdPage() {
this.idPage = document.head.querySelector("meta[name=idPage]").content;

View File

@ -205,6 +205,7 @@ window.AObject = class {
this.systemEvents = new Map();
this.customEvents = new Map();
this.parentEventMap = new Map();
this.listAObject = new Set();
this.eventName = window.getPrimaryPointerEvent();
}
@ -321,10 +322,13 @@ window.AObject = class {
element.removeEventListener(event, callback, false);
}
}
for (const [id, evM] of this.customEvents.entries()) {
for (const [id, obj] of this.customEvents.entries()) {
obj.ele.offById(id);
}
for (const id of this.listAObject) {
id.dispose();
}
this.listAObject.clear();
this.systemEvents.clear();
this.parentEventMap.clear();
@ -362,35 +366,47 @@ window.AObject = class {
removeSytemEventParent(parent) {
const entries = this.parentEventMap.get(parent);
if (!entries) return;
for (const { element, event } of entries) {
let iC = 0;
for (const { element, eventName } of entries) {
const eventMap = this.systemEvents.get(element);
if (eventMap) {
const data = eventMap.get(event);
const data = eventMap.get(eventName);
if (data && typeof data.callback === 'function') {
element.removeEventListener(event, data.callback, false);
eventMap.delete(event);
element.removeEventListener(eventName , data.callback, false);
eventMap.delete(eventName);
// Nếu element không còn event nào, xóa khỏi systemEvents
if (eventMap.size === 0) {
this.systemEvents.delete(element);
}
}
}
}
this.parentEventMap.delete(parent);
}
removeCustomEventParent(parent) {
const entries = this.parentEventMap.get("Custom" + parent);
if (!entries) return;
for (const { id, element } of entries) {
const eventMap = this.customEvents.get(id);
if (eventMap) {
eventMap.ele.offById(eventMap.id);
this.customEvents.delete(id);
}
}
this.parentEventMap.delete(parent);
}
addCustomEvent(id, element, groups = null) {
if (!this.customEvents.has(id)) {
this.customEvents.set(id, new Set());
this.customEvents.set(id, { "ele":element, "groups":groups });
}
const eventMap = this.customEvents.get(id);
eventMap.add(element, groups);
if (groups) {
if (!this.parentEventMap.has("Custom" + parent)) {
this.parentEventMap.set(("Custom" + parent, new Set());
this.parentEventMap.set("Custom" + parent, new Map());
}
this.parentEventMap.get("Custom" + parent).add({ id, element });
this.parentEventMap.get("Custom" + parent).set(id, element);
}
}
@ -509,9 +525,9 @@ class AApp extends window.AObject {
constructor(container = '[app-content]') {
super();
this.cachePage = new CacheManager();
this.currentLay = null;
this.isLoadedLayout = false;
window.Destroy = undefined;
window.ALayout = new Map();
this.isRedirectPage = false;
this.metaPage = document.head.querySelector("meta[name=idPage]");
this.pageName = this.metaPage.getAttribute("pageName");
@ -520,45 +536,40 @@ class AApp extends window.AObject {
this.mainApp = tmp.querySelector("[main-content]") ? tmp.querySelector("[main-content]") : tmp;
var f = function (ev) {
if (ev.state) {
const obj = this.cachePage.get(ev.state.url);
this.loadContentPage(obj.html);
this.metaPage.content = obj.idPage;
this.metaPage.setAttribute("isLayout", obj.isLayout);
this.cachePage.get(ev.state.url, ((result) => {
if (result == null) {
} else {
this.loadContentPage(result.html);
this.metaPage.content = result.idPage;
this.metaPage.setAttribute("isLayout", result.isLayout);
this.trigger("redirect_page", ev.state);
//this.checkLayout(obj.isLayout);
this.checkLayout(result.lName);
var l = new LoadScriptAsync("Page");
l.processScript(obj.doc);
l.processScript(result.doc);
l.on("Loaded", (() => {
this.loadedPage();
}).bind(this));
}
}).bind(this));
}
}.bind(this);
window.addEventListener("popstate", f);
}
checkLayout(lName) {
//if ((this.isDispLay && isLayout) == false) {
// if (isLayout) {
// if (window.isLoad_Menu) {
// window.Show_Menu();
// } else {
// this.renderLayout();
// }
// } else /*display =true -> layout false */ {
//
// }
//}
//this.isDispLay = isLayout;
if (lName === "None") {
window.Hide_Menu();
} else {
this.renderLayout();
if (this.lName !== lName) {
if (this.lName !== "None") {
const l = window.ALayout.get(this.lName);
l.dispose();
}
this.callLoadLayout(lName);
this.lName = lName;
}
}
render() {
if (this.lName !== "None") {
this.renderLayout();
this.callLoadLayout(this.lName);
} else {
this.isLoadedLayout = true;
}
@ -576,47 +587,7 @@ class AApp extends window.AObject {
this.callLoadPage(window.location.href);
}).bind(this)();
}
renderLayout() {
(function () {
var xhr = new XMLHttpRequest();
xhr.open("GET", this.addAttributeURL(window.location.href, "?vr=lAsync"));
xhr.send();
var f = function (evt) {
if (evt.currentTarget.readyState == 4 && evt.currentTarget.status == 200) {
if (evt.currentTarget.responseText) {
var jP = JSON.parse(evt.currentTarget.responseText);
this.loadLayout(jP, window.location.href);
}
}
}.bind(this);
xhr.addEventListener("readystatechange", f, false);
}).bind(this)();
}
loadedPage() {
this.metaPage = document.head.querySelector("meta[name=idPage]");
if (this.metaPage != null && this.isLoadedLayout) {
const tmp = this.cachePage.getInf(window.location.href);
if (tmp != null && tmp.dynamicF != null) {
if (window[tmp.dynamicF] != null) window[tmp.dynamicF]();
} else {
if (window["L" + this.metaPage.content] != null) window["L" + this.metaPage.content]();
}
this.initNavs(this.metaPage.content);
this.trigger("pageLoaded", null);
} else {
window.requestTimeout(this.loadedPage.bind(this), 10, window.registerCancel);
}
}
loadedLayout() {
if (!window.isLoad_Menu) {
window.Load_Menu();
this.isLoadedLayout = true;
}
this.trigger("layoutLoaded", null);
}
scrollTop() {
return window.scrollY || window.smScroll.scrollTop;
}
@ -686,15 +657,15 @@ class AApp extends window.AObject {
this.callLoadPage(window.GetAbsoluteURL(t));
}
callLoadPage(url) {
const page = this.cachePage.get(url);
if (page) {
this.setContentPage(page, url); // Set content page từ cache
this.cachePage.get(url, ((result) => {
if (result) {
this.setContentPage(result, url); // Set content page từ cache
} else {
console.log("connect new");
this.getPage(url); // Load mới nếu chưa có trong cache
}
}).bind(this));
}
loadContentPage(content) {
const tpl = document.createElement('template');
tpl.innerHTML = content;
@ -707,18 +678,17 @@ class AApp extends window.AObject {
document.title = page.title + " - " + this.pageName;
var meta = document.head.querySelector("meta[name=idPage]");
meta.content = page.idPage;
this.checkLayout(page.lName);
meta.setAttribute("layName", page.lName);
this.loadContentPage(page.html);
window.history.pushState({"url":url}, page.title, url);
}
setContentPage(page, url) {
this.contentPage(page, url);
var l = new LoadScriptAsync("Page");
if (this.isRedirectPage) {
this.trigger("redirect_page", page);
this.checkLayout(page.lName);
this.isRedirectPage = false;
}
l.on("Loaded", (() => {
@ -740,7 +710,83 @@ class AApp extends window.AObject {
}.bind(this);
xhr.addEventListener("readystatechange", f, false);
}
loadLayout(o, url) {
loadPage(o, url) {
const title = o.Title;
const idPage = o.PageId;
const doc2 = new DOMParser().parseFromString(o.Scripts, "text/html");
const doc = doc2.firstChild.querySelector("head");
const dF = doc.querySelector("script[dynamic]");
const obj = {
html: o.Content,
title: title,
idPage: idPage,
lName: o.LayoutName,
doc: doc,
dynamicF: dF ? dF.getAttribute("dynamic") : null
};
this.cachePage.set(url, obj);
this.setContentPage(obj, url);
}
loadedPage() {
this.metaPage = document.head.querySelector("meta[name=idPage]");
if (this.metaPage != null && this.isLoadedLayout) {
this.cachePage.getInf(window.location.href, ((tmp) => {
if (tmp != null && tmp.dynamicF != null) {
if (window[tmp.dynamicF] != null) window[tmp.dynamicF]();
} else {
if (window["L" + this.metaPage.content] != null) window["L" + this.metaPage.content]();
}
this.initNavs(this.metaPage.content);
this.trigger("pageLoaded", null);
}).bind(this));
} else {
window.requestTimeout(this.loadedPage.bind(this), 10, window.registerCancel);
}
}
callLoadLayout(lName) {
this.cachePage.get(lName, ((result) => {
if (result) {
this.setLayout(result); // Set content page từ cache
} else {
console.log("connect new Layout");
this.loadLayout(); // Load mới nếu chưa có trong cache
}
}).bind(this), "layout");
}
loadLayout() {
(function () {
var xhr = new XMLHttpRequest();
xhr.open("GET", this.addAttributeURL(window.location.href, "?vr=lAsync"));
xhr.send();
var f = function (evt) {
if (evt.currentTarget.readyState == 4 && evt.currentTarget.status == 200) {
if (evt.currentTarget.responseText) {
var jP = JSON.parse(evt.currentTarget.responseText);
this.setLayout(jP);
this.cachePage.set(this.lName, jP, "layout");
}
}
}.bind(this);
xhr.addEventListener("readystatechange", f, false);
}).bind(this)();
}
loadedLayout() {
if (this.lName !== "None") {
const l = window.ALayout.get(this.lName);
if (!l.isLoaded) {
console.log("render M");
l.renderMenu();
this.isLoadedLayout = true;
this.trigger("layoutLoaded", null);
}
}
}
setLayout(o) {
var oP = new DOMParser();
var pHtml = oP.parseFromString(o.Content, 'text/html');
(function () {
@ -765,22 +811,6 @@ class AApp extends window.AObject {
l.processScript(doc);
}).bind(this)();
}
loadPage(o, url) {
const title = o.Title;
const idPage = o.PageId;
const doc2 = new DOMParser().parseFromString(o.Scripts, "text/html");
const doc = doc2.firstChild.querySelector("head");
const dF = doc.querySelector("script[dynamic]");
const obj = {
html: o.Content,
title: title,
idPage: idPage,
doc: doc,
dynamicF: dF ? dF.getAttribute("dynamic"):null
};
this.cachePage.set(url, obj);
this.setContentPage(obj, url);
}
loadCSS(arr) {
for (var i = 0; i < arr.length; i++) {
var link = document.createElement('link');
@ -791,91 +821,258 @@ class AApp extends window.AObject {
}
}
class CacheManager {
constructor(prefix = "cache") {
this.prefix = prefix;
this.pageMap = new Map(); // Chứa key URL → idPage
this.layoutMap = new Map(); // Chứa key idLayout → true
this._loadFromStorage();
constructor() {
this.storage = new CacheStorage("appCache");
this.pageMap = new Map();
this.layoutMap = new Map();
this.readyCallbacks = [];
this.isReady = false;
this._loadIndex();
}
_key(id, type = "page") {
return `${this.prefix}|;${type}|;${id}`;
return `${type}|${id}`;
}
_loadFromStorage() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(this.prefix)) continue;
const [_, type, id] = key.split("|;");
_onReady(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
}
_loadIndex() {
this.storage.getAllKeys(((keys) => {
let pending = 0;
keys.forEach(((key) => {
if (!key.startsWith(`${this.storage.prefix}|`)) return;
const shortKey = key.replace(`${this.storage.prefix}|`, "");
const [type, id] = shortKey.split("|");
if (type === "pI") {
try {
const raw = JSON.parse(localStorage.getItem(key));
if (raw) {
this.pageMap.set(id, raw);
}
} catch (e) {
console.warn("Error parsing cached page:", e);
}
pending++;
this.storage.get(key, (data) => {
if (data) this.pageMap.set(id, data);
if (--pending === 0) this._finishInit();
}, true);
} else if (type === "layout") {
this.layoutMap.set(id, true);
}
}
}
getInf(id, type = "page") {
if (type === "page") {
return this.pageMap.get(id);
}
else {
return this.layoutMap.get(id);
}
}
// Lấy dữ liệu theo URL hoặc ID
get(id, type = "page") {
try {
if ((type === "page" && !this.pageMap.has(id)) || (type === "layout" && !this.layoutMap.has(id))) {
return null; // Nếu không có trong Map thì coi như không tồn tại
}).bind(this));
if (pending === 0) this._finishInit(); // Trường hợp không có key nào là "pI"
}).bind(this));
}
const raw = localStorage.getItem(this._key(id, type));
if (!raw) return null;
_finishInit() {
this.isReady = true;
this.readyCallbacks.forEach(cb => cb());
this.readyCallbacks = [];
}
const parsed = JSON.parse(raw);
parsed.doc = new DOMParser().parseFromString(parsed.doc, "text/html").head;
// Lấy thông tin trang hoặc layout
getInf(id, callback, type = "page") {
this._onReady(() => {
(type === "page") ? callback(this.pageMap.get(id)) : callback(this.layoutMap.get(id));
});
}
// Lấy dữ liệu từ cache (localStorage hoặc IndexedDB nếu cần)
get(id, callback, type = "page") {
this._onReady(() => {
const key = this._key(id, type);
this.storage.get(key, (data) => {
if (!data) {
callback(null);
return;
}
if (type === "page") {
return Object.assign(parsed, this.pageMap.get(id), { url: id });
const pInfo = this.pageMap.get(id);
if (!pInfo) {
callback(null);
return;
}
data.doc = new DOMParser().parseFromString(data.doc, "text/html").head;
callback(Object.assign(data, pInfo, { url: id }));
} else {
return parsed;
callback(data);
}
} catch (e) {
console.warn("Cache parse error:", e);
return null;
});
});
}
}
// Lưu dữ liệu page/layout theo ID
set(id, obj, type = "page") {
try {
const clone = {};
// Lưu dữ liệu page hoặc layout vào cache
set(id, obj, type = "page", callback) {
this._onReady(() => {
const key = this._key(id, type);
if (type === "page") {
clone.html = obj.html;
clone.doc = obj.doc.innerHTML;
const pI = {
const htmlData = {
html: obj.html,
doc: obj.doc.innerHTML
};
// Lưu dữ liệu htmlData vào storage
this.storage.set(key, htmlData, function () { });
const info = {
title: obj.title,
idPage: obj.idPage,
lName: obj.lName,
dynamicF: obj.dynamicF
};
localStorage.setItem(this._key(id, "pI"), JSON.stringify(pI))
localStorage.setItem(this._key(id, type), JSON.stringify(clone));
this.pageMap.set(id, pI);
}
else
{
// Lưu thông tin trang vào pageMap và storage
this.pageMap.set(id, info);
this.storage.set(`pI|${id}`, info, function () { });
} else {
const layout = {
Content: obj.Content,
Scripts: obj.Scripts
};
// Lưu dữ liệu layout vào storage
this.layoutMap.set(id, true);
this.storage.set(key, layout, function () {
// callback(null); // Gọi callback khi lưu thành công
});
}
});
}
}
class CacheStorage {
constructor(prefix = "cache", maxAgeMs = 3 * 24 * 60 * 60 * 1000) {
this.prefix = prefix;
this.maxAgeMs = maxAgeMs;
this.db = null;
this.useIndexedDB = false;
this.queue = [];
this.ready = false;
this._initDB();
this.cleanupExpired();
}
_key(key) {
return `${this.prefix}|${key}`;
}
_initDB() {
const req = indexedDB.open(`${this.prefix}_db`, 1);
req.onupgradeneeded = (e) => {
const store = e.target.result.createObjectStore("cache", { keyPath: "key" });
try {
store.add({ id: 1, value: "test data" });
store.delete("1");
} catch (e) {
console.warn("Failed to store cache:", e);
this.ready = true;
this.db = null;
this.useIndexedDB = false;
}
};
req.onsuccess = (event) => {
this.db = req.result;
this.ready = true;
this._flushQueue();
};
req.onerror = () => {
console.warn("IndexedDB failed, fallback to localStorage");
this.ready = true;
this.db = null;
this._flushQueue();
};
}
_flushQueue() {
while (this.queue.length) {
const fn = this.queue.shift();
fn();
}
}
_onReady(fn) {
if (this.ready) fn();
else this.queue.push(fn);
}
get(key, callback, f=false) {
this._onReady(() => {
if (this.db) {
const tx = this.db.transaction("cache", "readonly");
const store = tx.objectStore("cache");
const req = store.get(f ? key : this._key(key));
req.onsuccess = () => callback(req.result ? req.result.value : null);
req.onerror = () => callback(null);
} else {
try {
const raw = localStorage.getItem(this._key(key));
const parsed = raw ? JSON.parse(raw) : null;
callback(parsed.data);
} catch {
callback(null);
}
}
});
}
getAllKeys(callback) {
this._onReady(() => {
if (this.db) {
const tx = this.db.transaction("cache", "readonly");
const store = tx.objectStore("cache");
const req = store.getAllKeys();
req.onsuccess = () => {
callback(req.result);
};
}
});
}
set(key, data, callback = () => { }) {
this._onReady(() => {
if (this.db) {
const tx = this.db.transaction("cache", "readwrite");
const store = tx.objectStore("cache");
store.put({ key: this._key(key), value: data, ts: Date.now() });
tx.oncomplete = () => callback(null);
tx.onerror = (e) => callback(e);
} else {
try {
console.log("Run Local");
localStorage.setItem(this._key(key), JSON.stringify({ data, ts: Date.now() }));
callback(null);
} catch (e) {
callback(e);
}
}
});
}
remove(key) {
const fullKey = this._key(key);
localStorage.removeItem(fullKey);
if (this.db) {
try {
const tx = this.db.transaction("cache", "readwrite");
const store = tx.objectStore("cache");
store.delete(fullKey);
} catch { }
}
}
cleanupExpired() {
const now = Date.now();
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (!key.startsWith(this.prefix)) continue;
try {
const raw = JSON.parse(localStorage.getItem(key));
if (raw && raw.ts && now - raw.ts > this.maxAgeMs) {
localStorage.removeItem(key);
}
} catch {
localStorage.removeItem(key);
}
}
}
}