@nova-fe/i18next-cache-backend
Version:
强大的 i18next 后端插件,具有 IndexedDB 缓存、批量加载和智能缓存策略
403 lines (402 loc) • 11.5 kB
JavaScript
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
};