UNPKG

@nova-fe/i18next-cache-backend

Version:

强大的 i18next 后端插件,具有 IndexedDB 缓存、批量加载和智能缓存策略

403 lines (402 loc) 11.5 kB
var f = Object.defineProperty; var g = (n, e, t) => e in n ? f(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t; var i = (n, e, t) => g(n, typeof e != "symbol" ? e + "" : e, t); import { openDB as m } from "idb"; import d from "ky"; class u { constructor(e = 100) { i(this, "cache", /* @__PURE__ */ new Map()); i(this, "maxSize"); this.maxSize = e; } async get(e) { const t = this.cache.get(e); return t ? Date.now() > t.timestamp + t.ttl ? (this.cache.delete(e), null) : t : null; } async set(e, t) { if (this.cache.size >= this.maxSize && !this.cache.has(e)) { const s = this.cache.keys().next().value; s && this.cache.delete(s); } this.cache.set(e, t); } async delete(e) { this.cache.delete(e); } async clear() { this.cache.clear(); } async keys() { return Array.from(this.cache.keys()); } async size() { return this.cache.size; } // Clean expired entries async cleanup() { const e = Date.now(); for (const [t, s] of this.cache.entries()) e > s.timestamp + s.ttl && this.cache.delete(t); } } class y { constructor(e = "i18next-cache", t = 1) { i(this, "db", null); i(this, "dbName"); i(this, "version"); this.dbName = e, this.version = t; } async getDB() { return this.db || (this.db = await m(this.dbName, this.version, { upgrade(e) { e.objectStoreNames.contains("translations") || e.createObjectStore("translations"); } })), this.db; } async get(e) { try { const s = await (await this.getDB()).get("translations", e); return s ? Date.now() > s.timestamp + s.ttl ? (await this.delete(e), null) : s : null; } catch (t) { return console.error("IDBCache get error:", t), null; } } async set(e, t) { try { await (await this.getDB()).put("translations", t, e); } catch (s) { console.error("IDBCache set error:", s); } } async delete(e) { try { await (await this.getDB()).delete("translations", e); } catch (t) { console.error("IDBCache delete error:", t); } } async clear() { try { await (await this.getDB()).clear("translations"); } catch (e) { console.error("IDBCache clear error:", e); } } async keys() { try { return await (await this.getDB()).getAllKeys("translations"); } catch (e) { return console.error("IDBCache keys error:", e), []; } } async size() { try { return await (await this.getDB()).count("translations"); } catch (e) { return console.error("IDBCache size error:", e), 0; } } // Clean expired entries async cleanup() { try { const t = (await this.getDB()).transaction("translations", "readwrite"), a = await t.objectStore("translations").openCursor(), c = Date.now(); for (; a; ) { const r = a.value; c > r.timestamp + r.ttl && await a.delete(), await a.continue(); } await t.done; } catch (e) { console.error("IDBCache cleanup error:", e); } } } class p { constructor(e = 50, t = "i18next-cache", s = 1) { i(this, "memoryCache"); i(this, "persistentCache"); this.memoryCache = new u(e), this.persistentCache = new y(t, s); } async get(e) { let t = await this.memoryCache.get(e); return t || (t = await this.persistentCache.get(e), t ? (await this.memoryCache.set(e, t), t) : null); } async set(e, t) { await Promise.all([ this.memoryCache.set(e, t), this.persistentCache.set(e, t) ]); } async delete(e) { await Promise.all([ this.memoryCache.delete(e), this.persistentCache.delete(e) ]); } async clear() { await Promise.all([this.memoryCache.clear(), this.persistentCache.clear()]); } async keys() { return await this.persistentCache.keys(); } async size() { return await this.persistentCache.size(); } async cleanup() { await Promise.all([ this.memoryCache.cleanup(), this.persistentCache.cleanup() ]); } // Get cache statistics async getStats() { const [e, t] = await Promise.all([ this.memoryCache.size(), this.persistentCache.size() ]); return { memory: { size: e, maxSize: this.memoryCache.maxSize }, persistent: { size: t } }; } } class l { constructor(e) { i(this, "client"); i(this, "config"); i(this, "requestQueue", /* @__PURE__ */ new Map()); this.config = e, this.client = d.create({ prefixUrl: `http://${e.server.host}:${e.server.port}${e.server.apiPrefix}`, timeout: e.performance.timeout, retry: { limit: e.performance.retryAttempts, methods: ["get"], statusCodes: [408, 413, 429, 500, 502, 503, 504] } }); } getCacheKey(e) { return "language" in e ? `${e.language}:${e.namespace}` : `bulk:${e.languages.join(",")}:${e.namespaces.join(",")}`; } async makeRequest(e, t) { if (this.requestQueue.has(t)) return this.requestQueue.get(t); const s = this.client.get(e).json(); this.requestQueue.set(t, s); try { return await s; } finally { this.requestQueue.delete(t); } } async fetchNamespace(e) { const { language: t, namespace: s } = e, a = `${t}/${s}`, c = this.getCacheKey(e); return this.config.debug && console.log(`Fetching namespace: ${t}/${s}`), this.makeRequest(a, c); } async fetchBulk(e) { const t = "all", s = this.getCacheKey(e); return this.config.debug && console.log("Fetching bulk data"), this.makeRequest(t, s); } async fetchIncremental(e, t, s) { const a = `${e}/${t}/incremental`, c = s ? { version: s } : void 0; this.config.debug && console.log( `Fetching incremental: ${e}/${t} from version ${s}` ); const r = c ? { searchParams: c } : {}; return this.client.get(a, r).json(); } // Health check async ping() { try { return await this.client.get("health").json(), !0; } catch { return !1; } } // Get server info async getServerInfo() { return this.client.get("info").json(); } // Clear request queue (useful for testing) clearRequestQueue() { this.requestQueue.clear(); } } class w { constructor(e, t) { i(this, "type", "backend"); i(this, "config"); i(this, "cache"); i(this, "httpClient"); i(this, "_services"); i(this, "_isInitialized", !1); i(this, "bulkDataCache", /* @__PURE__ */ new Map()); this._services = e, this.config = this.mergeConfig(t), this.cache = this.createCacheProvider(), this.httpClient = new l(this.config); } mergeConfig(e) { return { ...{ server: { port: 3001, host: "localhost", apiPrefix: "/api" }, cache: { storage: "indexeddb", maxSize: 52428800, // 50MB ttl: 864e5, // 24 hours namespace: "i18next-cache", version: "1.0.0" }, fetchStrategy: "namespace", performance: { batchSize: 10, retryAttempts: 3, timeout: 1e4, concurrency: 5 }, debug: !1, offline: !1 }, ...e }; } createCacheProvider() { const { storage: e, namespace: t } = this.config.cache; switch (e) { case "localstorage": return new u(100); // Fallback to memory for localStorage case "indexeddb": default: return new p(50, t); } } init(e, t, s) { this._services = e, this.config = this.mergeConfig(t), this.cache = this.createCacheProvider(), this.httpClient = new l(this.config), this._isInitialized = !0, this.config.debug && console.log("CacheBackend initialized with config:", this.config), this.startCleanupInterval(); } startCleanupInterval() { setInterval( () => { this.cache.cleanup && this.cache.cleanup(); }, 60 * 60 * 1e3 ); } generateCacheKey(e, t) { return `${this.config.cache.namespace}:${e}:${t}`; } async read(e, t, s) { try { const a = this.generateCacheKey(e, t), c = await this.cache.get(a); if (c && this.isCacheValid(c)) { this.config.debug && console.log(`Cache hit for ${e}/${t}`), s(null, c.data); return; } this.config.debug && console.log( `Cache miss for ${e}/${t}, fetching from server` ); const r = await this.fetchTranslations(e, t), o = { data: r, timestamp: Date.now(), version: this.config.cache.version, ttl: this.config.cache.ttl }; await this.cache.set(a, o), s(null, r); } catch (a) { if (this.config.debug && console.error(`Error reading ${e}/${t}:`, a), this.config.offline) { const c = this.generateCacheKey(e, t), r = await this.cache.get(c); if (r) { s(null, r.data); return; } } s(a, null); } } isCacheValid(e) { const s = Date.now() > e.timestamp + e.ttl, a = !this.config.cache.version || e.version === this.config.cache.version; return !s && a; } async fetchTranslations(e, t) { const s = { language: e, namespace: t }; return this.config.fetchStrategy === "bulk" ? await this.fetchFromBulk(e, t) : await this.httpClient.fetchNamespace(s); } async fetchFromBulk(e, t) { const s = "bulk:all"; if (this.bulkDataCache.has(s)) this.config.debug && console.log("Using cached bulk request"); else { this.config.debug && console.log("Creating bulk request for all languages and namespaces"); const c = this.httpClient.fetchBulk({ languages: ["en", "zh", "es", "fr"], // All supported languages namespaces: ["common", "navigation", "forms"] // All namespaces }).then(async (r) => (await this.cacheAllBulkData(r), r)); this.bulkDataCache.set(s, c); } return (await this.bulkDataCache.get(s))[e]?.[t] || {}; } async cacheAllBulkData(e) { this.config.debug && console.log("Caching all bulk data to IndexedDB..."); const t = []; for (const [s, a] of Object.entries(e)) for (const [c, r] of Object.entries(a)) { const o = this.generateCacheKey(s, c), h = { data: r, timestamp: Date.now(), version: this.config.cache.version, ttl: this.config.cache.ttl }; t.push(this.cache.set(o, h)); } await Promise.all(t), this.config.debug && console.log( `Cached ${t.length} language/namespace combinations to IndexedDB` ); } // Public API methods async preload(e, t) { const s = []; for (const a of e) for (const c of t) s.push( new Promise((r, o) => { this.read(a, c, (h) => { h ? o(h) : r(); }); }) ); await Promise.allSettled(s); } async clearCache() { await this.cache.clear(); } async getCacheStats() { const e = await this.cache.size(), t = await this.cache.keys(); return { size: e, entries: t.length, keys: t }; } async warmup(e, t) { this.config.debug && console.log("Warming up cache for:", { languages: e, namespaces: t }), await this.preload(e, t); } // Health check async isServerHealthy() { return await this.httpClient.ping(); } } i(w, "type", "backend"); export { w as CacheBackend, p as DualCache, l as HttpClient, y as IDBCache, u as MemoryCache, w as default };