@lacqjs/nuxt-dict 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +0 -0
  2. package/dist/module.d.mts +13 -0
  3. package/dist/module.d.ts +13 -0
  4. package/dist/module.json +13 -0
  5. package/dist/module.mjs +83 -0
  6. package/dist/runtime/composables/useDict.d.ts +14 -0
  7. package/dist/runtime/composables/useDict.js +59 -0
  8. package/dist/runtime/composables/useDictOptions.d.ts +13 -0
  9. package/dist/runtime/composables/useDictOptions.js +17 -0
  10. package/dist/runtime/composables/useDictTree.d.ts +13 -0
  11. package/dist/runtime/composables/useDictTree.js +53 -0
  12. package/dist/runtime/composables/useLocale.d.ts +10 -0
  13. package/dist/runtime/composables/useLocale.js +20 -0
  14. package/dist/runtime/core/adapter.d.ts +17 -0
  15. package/dist/runtime/core/adapter.js +50 -0
  16. package/dist/runtime/core/cache/indexeddb-cache.d.ts +48 -0
  17. package/dist/runtime/core/cache/indexeddb-cache.js +142 -0
  18. package/dist/runtime/core/cache/memory-cache.d.ts +25 -0
  19. package/dist/runtime/core/cache/memory-cache.js +79 -0
  20. package/dist/runtime/core/cache/version-check.d.ts +26 -0
  21. package/dist/runtime/core/cache/version-check.js +38 -0
  22. package/dist/runtime/core/dict-manager.d.ts +74 -0
  23. package/dist/runtime/core/dict-manager.js +217 -0
  24. package/dist/runtime/options.d.ts +3 -0
  25. package/dist/runtime/options.js +33 -0
  26. package/dist/runtime/plugins/dict.d.ts +2 -0
  27. package/dist/runtime/plugins/dict.js +130 -0
  28. package/dist/runtime/types/index.d.ts +171 -0
  29. package/dist/runtime/types/index.js +0 -0
  30. package/dist/runtime/utils/dict-translator.d.ts +34 -0
  31. package/dist/runtime/utils/dict-translator.js +30 -0
  32. package/dist/runtime/utils/logger.d.ts +10 -0
  33. package/dist/runtime/utils/logger.js +33 -0
  34. package/dist/types.d.mts +3 -0
  35. package/package.json +88 -0
package/README.md ADDED
File without changes
@@ -0,0 +1,13 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import * as __runtime_types from '../dist/runtime/types/index.js';
3
+ import { ModuleOptions } from '../dist/runtime/types/index.js';
4
+ export { ModuleOptions } from '../dist/runtime/types/index.js';
5
+
6
+ /**
7
+ * Nuxt Dict 模块入口。
8
+ * 提供字典数据的统一管理、缓存、翻译和 SSR 预取能力。
9
+ * 用户通过 nuxt.config.ts 的 `dict` 配置项进行定制。
10
+ */
11
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, __runtime_types.ResolvedModuleOptions, true>;
12
+
13
+ export { _default as default };
@@ -0,0 +1,13 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import * as __runtime_types from '../dist/runtime/types/index.js';
3
+ import { ModuleOptions } from '../dist/runtime/types/index.js';
4
+ export { ModuleOptions } from '../dist/runtime/types/index.js';
5
+
6
+ /**
7
+ * Nuxt Dict 模块入口。
8
+ * 提供字典数据的统一管理、缓存、翻译和 SSR 预取能力。
9
+ * 用户通过 nuxt.config.ts 的 `dict` 配置项进行定制。
10
+ */
11
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, __runtime_types.ResolvedModuleOptions, true>;
12
+
13
+ export { _default as default };
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@lacqjs/nuxt-dict",
3
+ "version": "0.0.2",
4
+ "configKey": "dict",
5
+ "compatibility": {
6
+ "nuxt": "^4.4.8"
7
+ },
8
+ "moduleDependencies": {},
9
+ "builder": {
10
+ "@nuxt/module-builder": "1.0.2",
11
+ "unbuild": "3.6.1"
12
+ }
13
+ }
@@ -0,0 +1,83 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { defineNuxtModule, useLogger, createResolver, addPlugin, addImportsDir, addTypeTemplate } from '@nuxt/kit';
3
+ import { defaultOptions } from '../dist/runtime/options.js';
4
+ import { createLogger } from '../dist/runtime/utils/logger.js';
5
+
6
+ const name = "@lacqjs/nuxt-dict";
7
+ const version = "0.0.2";
8
+ const devDependencies = {
9
+ nuxt: "^4.4.8"};
10
+ const pkg = {
11
+ name: name,
12
+ version: version,
13
+ devDependencies: devDependencies};
14
+
15
+ function registerTypeTemplates(resolver, stores) {
16
+ addTypeTemplate({
17
+ filename: "types/nuxt-dict.d.ts",
18
+ getContents: () => `
19
+ import type { DictManager } from '${pkg.name}'
20
+
21
+ declare module '#app' {
22
+ interface NuxtApp {
23
+ $dict: ReturnType<typeof import('${pkg.name}').createDictTranslator>
24
+ $dictManager: DictManager
25
+ }
26
+ }
27
+
28
+ export {}
29
+ `
30
+ });
31
+ addTypeTemplate({
32
+ filename: "types/nuxt-dict-store-names.d.ts",
33
+ getContents: () => {
34
+ const storeNames = ["dicts", ...Object.keys(stores ?? {})];
35
+ const union = storeNames.map((s) => `'${s}'`).join(" | ");
36
+ return `export type StoreKey = ${union}
37
+ `;
38
+ }
39
+ });
40
+ }
41
+ const module$1 = defineNuxtModule().with({
42
+ meta: {
43
+ name: pkg.name,
44
+ version: pkg.version,
45
+ configKey: "dict",
46
+ compatibility: {
47
+ nuxt: pkg.devDependencies.nuxt
48
+ },
49
+ moduleDependencies: {}
50
+ },
51
+ defaults: defaultOptions,
52
+ setup(_options, _nuxt) {
53
+ const logger = useLogger(pkg.name, { level: _options.logLevel });
54
+ logger.debug(`\u521D\u59CB\u5316\u6A21\u5757 ${pkg.name}`);
55
+ logger.debug(`${pkg.name} \u6A21\u5757\u9009\u9879:`, _options);
56
+ if (!_options.enable) {
57
+ logger.debug(`${pkg.name} \u6A21\u5757\u88AB\u7981\u7528\uFF0C\u8DF3\u8FC7\u8BBE\u7F6E\u3002`);
58
+ return;
59
+ }
60
+ const resolver = createResolver(import.meta.url);
61
+ const runtimeDir = fileURLToPath(new URL("./runtime", import.meta.url));
62
+ _nuxt.options.runtimeConfig.public.dict = _options;
63
+ _nuxt.options.build.transpile.push(resolver.resolve(runtimeDir));
64
+ addPlugin({ src: resolver.resolve(runtimeDir, "plugins", "dict.ts") });
65
+ addImportsDir(resolver.resolve(runtimeDir, "composables"));
66
+ registerTypeTemplates(resolver, _options.stores);
67
+ _nuxt.hook("prepare:types", ({ references }) => {
68
+ references.push({ types: pkg.name });
69
+ });
70
+ },
71
+ /** 模块首次安装到项目时触发 */
72
+ onInstall(_nuxt) {
73
+ const logger = createLogger(pkg.name);
74
+ logger.info(`\u9996\u6B21\u4E3A ${pkg.name} \u8FDB\u884C\u8BBE\u7F6E\uFF01`);
75
+ },
76
+ /** 模块版本升级时触发(使用 semver 比较,每版本只触发一次) */
77
+ onUpgrade(_nuxt, _options, _previousVersion) {
78
+ const logger = createLogger(pkg.name, { level: _options.logLevel });
79
+ logger.info(`\u5347\u7EA7 ${pkg.name} \u4ECE ${_previousVersion} \u5230 ${pkg.version}`);
80
+ }
81
+ });
82
+
83
+ export { module$1 as default };
@@ -0,0 +1,14 @@
1
+ import type { UseDictReturn, StoreKey } from '../types/index.js';
2
+ /**
3
+ * 使用指定类型的字典数据。
4
+ * 组件挂载时自动加载,卸载时自动清理引用。
5
+ * 返回翻译函数、加载状态和手动刷新方法。
6
+ *
7
+ * @example
8
+ * // 默认存储库 'dicts'
9
+ * const { data, translate } = useDict('gender')
10
+ * // 指定存储库 'dicts2'
11
+ * const { data, translate } = useDict('dicts2', 'gender')
12
+ */
13
+ export declare function useDict(type: string): UseDictReturn;
14
+ export declare function useDict(storeName: StoreKey, type: string): UseDictReturn;
@@ -0,0 +1,59 @@
1
+ import { shallowRef, ref, watch, onMounted, onBeforeUnmount } from "vue";
2
+ import { useNuxtApp } from "#imports";
3
+ import { DEFAULT_STORE_NAME } from "../core/cache/indexeddb-cache.js";
4
+ const activeInstances = /* @__PURE__ */ new Map();
5
+ async function fetchDictData(manager, dictType, storeName, data, loading, error, mode = "load") {
6
+ loading.value = true;
7
+ error.value = null;
8
+ try {
9
+ const entry = await (mode === "refresh" ? manager.refresh(dictType, storeName) : manager.getDict(dictType, storeName));
10
+ data.value = entry.items;
11
+ } catch (e) {
12
+ error.value = e instanceof Error ? e.message : String(e);
13
+ } finally {
14
+ loading.value = false;
15
+ }
16
+ }
17
+ function trackInstance(type, instanceId) {
18
+ if (!activeInstances.has(type)) {
19
+ activeInstances.set(type, /* @__PURE__ */ new Set());
20
+ }
21
+ activeInstances.get(type).add(instanceId);
22
+ onBeforeUnmount(() => {
23
+ const instances = activeInstances.get(type);
24
+ if (instances) {
25
+ instances.delete(instanceId);
26
+ if (instances.size === 0) {
27
+ activeInstances.delete(type);
28
+ }
29
+ }
30
+ });
31
+ }
32
+ export function useDict(storeOrType, maybeType) {
33
+ const nuxtApp = useNuxtApp();
34
+ const manager = nuxtApp.$dictManager;
35
+ const storeName = maybeType === void 0 ? DEFAULT_STORE_NAME : storeOrType;
36
+ const dictType = maybeType ?? storeOrType;
37
+ const data = shallowRef(null);
38
+ const loading = ref(false);
39
+ const error = ref(null);
40
+ const instanceId = Symbol(dictType);
41
+ const trackKey = `${storeName}:${dictType}`;
42
+ trackInstance(trackKey, instanceId);
43
+ function translate(code) {
44
+ return manager.translate(dictType, code, storeName);
45
+ }
46
+ async function refresh() {
47
+ await fetchDictData(manager, dictType, storeName, data, loading, error, "refresh");
48
+ }
49
+ onMounted(() => {
50
+ fetchDictData(manager, dictType, storeName, data, loading, error, "load");
51
+ });
52
+ watch(
53
+ () => manager.locale.value,
54
+ () => {
55
+ fetchDictData(manager, dictType, storeName, data, loading, error, "load");
56
+ }
57
+ );
58
+ return { data, translate, loading, error, refresh };
59
+ }
@@ -0,0 +1,13 @@
1
+ import type { UseDictOptionsReturn, StoreKey } from '../types/index.js';
2
+ /**
3
+ * 以 { label, value } 格式使用字典数据,
4
+ * 直接适配 UI 库的 options 属性。
5
+ *
6
+ * @example
7
+ * // 默认存储库 'dicts'
8
+ * const { options } = useDictOptions('industry')
9
+ * // 指定存储库 'dicts2'
10
+ * const { options } = useDictOptions('dicts2', 'industry')
11
+ */
12
+ export declare function useDictOptions(type: string): UseDictOptionsReturn;
13
+ export declare function useDictOptions(storeName: StoreKey, type: string): UseDictOptionsReturn;
@@ -0,0 +1,17 @@
1
+ import { computed } from "vue";
2
+ import { useDict } from "./useDict.js";
3
+ export function useDictOptions(storeOrType, maybeType) {
4
+ const { data, loading, refresh } = maybeType === void 0 ? useDict(storeOrType) : useDict(storeOrType, maybeType);
5
+ const options = computed(() => {
6
+ if (!data.value) return [];
7
+ return data.value.map((item) => ({
8
+ label: item.label,
9
+ value: item.code
10
+ }));
11
+ });
12
+ return {
13
+ options,
14
+ loading,
15
+ refresh
16
+ };
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { UseDictTreeReturn, StoreKey } from '../types/index.js';
2
+ /**
3
+ * 使用树形字典数据,支持翻译和路径查找。
4
+ * 组件挂载时自动加载,适用于区域选择器等级联场景。
5
+ *
6
+ * @example
7
+ * // 默认存储库 'dicts'
8
+ * const { tree, translate } = useDictTree('region')
9
+ * // 指定存储库 'dicts2'
10
+ * const { tree, translate } = useDictTree('dicts2', 'region')
11
+ */
12
+ export declare function useDictTree(type: string): UseDictTreeReturn;
13
+ export declare function useDictTree(storeName: StoreKey, type: string): UseDictTreeReturn;
@@ -0,0 +1,53 @@
1
+ import { shallowRef, ref, watch, onMounted } from "vue";
2
+ import { useNuxtApp } from "#imports";
3
+ import { DEFAULT_STORE_NAME } from "../core/cache/indexeddb-cache.js";
4
+ export function useDictTree(storeOrType, maybeType) {
5
+ const nuxtApp = useNuxtApp();
6
+ const manager = nuxtApp.$dictManager;
7
+ const storeName = maybeType === void 0 ? DEFAULT_STORE_NAME : storeOrType;
8
+ const dictType = maybeType ?? storeOrType;
9
+ const tree = shallowRef(null);
10
+ const loading = ref(false);
11
+ function translate(code) {
12
+ return manager.translate(dictType, code, storeName);
13
+ }
14
+ function findPath(code) {
15
+ if (!tree.value) return [];
16
+ return findPathInTree(tree.value, code);
17
+ }
18
+ async function load() {
19
+ loading.value = true;
20
+ try {
21
+ const entry = await manager.getDict(dictType, storeName);
22
+ tree.value = entry.tree ?? null;
23
+ } finally {
24
+ loading.value = false;
25
+ }
26
+ }
27
+ async function refresh() {
28
+ loading.value = true;
29
+ try {
30
+ const entry = await manager.refresh(dictType, storeName);
31
+ tree.value = entry.tree ?? null;
32
+ } finally {
33
+ loading.value = false;
34
+ }
35
+ }
36
+ onMounted(load);
37
+ watch(() => manager.locale.value, load);
38
+ return { tree, translate, findPath, loading, refresh };
39
+ }
40
+ function findPathInTree(nodes, targetCode) {
41
+ for (const node of nodes) {
42
+ if (node.code === targetCode) {
43
+ return [node.label];
44
+ }
45
+ if (node.children && node.children.length > 0) {
46
+ const childPath = findPathInTree(node.children, targetCode);
47
+ if (childPath.length > 0) {
48
+ return [node.label, ...childPath];
49
+ }
50
+ }
51
+ }
52
+ return [];
53
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 获取/切换当前语言。
3
+ * 切换语言时会同步更新 cookie(客户端)并通知 DictManager 刷新缓存。
4
+ * locale 为 DictManager 上的响应式 ref,语言切换后所有 useDict / useDictTree 组件自动重取。
5
+ */
6
+ export declare function useLocale(): {
7
+ locale: import("vue").ShallowRef<string, string>;
8
+ setLocale: (newLocale: string) => void;
9
+ locales: string[];
10
+ };
@@ -0,0 +1,20 @@
1
+ import { useNuxtApp, useCookie, useRuntimeConfig } from "#imports";
2
+ import { defaultOptions } from "../options.js";
3
+ export function useLocale() {
4
+ const nuxtApp = useNuxtApp();
5
+ const manager = nuxtApp.$dictManager;
6
+ const options = useRuntimeConfig().public.dict ?? defaultOptions;
7
+ function setLocale(newLocale) {
8
+ manager.setLocale(newLocale);
9
+ if (import.meta.client) {
10
+ const langCookie = useCookie(options.locale.cookieKey, { maxAge: 60 * 60 * 24 * 365 });
11
+ langCookie.value = newLocale;
12
+ }
13
+ }
14
+ const locales = [];
15
+ return {
16
+ locale: manager.locale,
17
+ setLocale,
18
+ locales
19
+ };
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { DictAdapter } from '../types/index.js';
2
+ /** 创建默认适配器所需的配置参数 */
3
+ export interface DefaultAdapterOptions {
4
+ baseURL: string;
5
+ dictEndpoint: string;
6
+ versionEndpoint: string;
7
+ /** 发送给 API 的语言参数名,设为空则不传,默认 'lang' */
8
+ paramKey: string;
9
+ /** 发送给 API 的语言请求头名,设为空则不传,默认 'X-Locale' */
10
+ apiHeaderKey: string;
11
+ }
12
+ /**
13
+ * 创建默认的 REST 字典适配器。
14
+ * 底层使用原生 fetch,兼容浏览器和 Node.js 18+(Nuxt 4 要求)。
15
+ * SSR 侧由 resolveBaseURL() 保证 baseURL 为绝对 origin,客户端相对路径由浏览器处理。
16
+ */
17
+ export declare function createDefaultAdapter(options: DefaultAdapterOptions): DictAdapter;
@@ -0,0 +1,50 @@
1
+ function buildURL(baseURL, endpoint, params) {
2
+ const searchParams = new URLSearchParams(params);
3
+ const qs = searchParams.toString();
4
+ return qs ? `${baseURL}${endpoint}?${qs}` : `${baseURL}${endpoint}`;
5
+ }
6
+ async function fetchDictImpl(_storeName, config, types, locale) {
7
+ const params = { types: types.join(",") };
8
+ if (config.paramKey) {
9
+ params[config.paramKey] = locale;
10
+ }
11
+ const url = buildURL(config.baseURL, config.dictEndpoint, params);
12
+ const headers = { Accept: "application/json" };
13
+ if (config.apiHeaderKey) {
14
+ headers[config.apiHeaderKey] = locale;
15
+ }
16
+ try {
17
+ const response = await fetch(url, { method: "GET", headers });
18
+ if (!response.ok) {
19
+ throw new Error(`HTTP ${response.status}`);
20
+ }
21
+ return response.json();
22
+ } catch (e) {
23
+ throw new Error(`Failed to fetch dictionary: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
24
+ }
25
+ }
26
+ async function fetchVersionImpl(_storeName, config) {
27
+ const url = buildURL(config.baseURL, config.versionEndpoint, {});
28
+ try {
29
+ const response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP ${response.status}`);
32
+ }
33
+ const data = await response.json();
34
+ if (!data.version) {
35
+ throw new Error("Version not found in response");
36
+ }
37
+ return data.version;
38
+ } catch (e) {
39
+ throw new Error(`Failed to fetch version: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
40
+ }
41
+ }
42
+ export function createDefaultAdapter(options) {
43
+ const { baseURL, dictEndpoint, versionEndpoint, paramKey, apiHeaderKey } = options;
44
+ const dictConfig = { baseURL, dictEndpoint, paramKey, apiHeaderKey };
45
+ const versionConfig = { baseURL, versionEndpoint };
46
+ return {
47
+ fetchDict: (storeName, { types, locale }) => fetchDictImpl(storeName, dictConfig, types, locale),
48
+ fetchVersion: (storeName) => fetchVersionImpl(storeName, versionConfig)
49
+ };
50
+ }
@@ -0,0 +1,48 @@
1
+ import type { DictEntry, CacheEntry } from '../../types/index.js';
2
+ /** 默认 IndexedDB 对象存储库名称 */
3
+ export declare const DEFAULT_STORE_NAME = "dicts";
4
+ /**
5
+ * IndexedDB 缓存实现,用于持久化字典数据。
6
+ * 支持客户端离线访问,减少重复网络请求。
7
+ * 版本号改用 localStorage 存储,不再使用 IndexedDB。
8
+ *
9
+ * 多对象存储库支持:
10
+ * - 默认存储库 `'dicts'` 在 init() 时创建
11
+ * - 额外存储库通过 ensureStore() 惰性创建
12
+ * - 使用串行升级队列处理并发多 store 创建,批量累积后一次升级完成
13
+ */
14
+ export declare class IndexedDBCache {
15
+ private dbName;
16
+ private db;
17
+ private version;
18
+ /** 串行化升级队列,防止并发 ensureStore 导致多次版本升级 */
19
+ private upgradeQueue;
20
+ /** 累积待创建的 store 名,批量创建以减少升级次数 */
21
+ private pendingStores;
22
+ constructor(dbName: string);
23
+ /** 初始化 IndexedDB,创建默认对象存储库 */
24
+ init(): Promise<void>;
25
+ /**
26
+ * 惰性确保指定名称的对象存储库存在。
27
+ * 若当前 DB 中已有该 store 则立即返回;
28
+ * 否则将 store 名加入待建集合,通过串行队列触发一次版本升级批量创建。
29
+ */
30
+ private ensureStore;
31
+ /**
32
+ * 执行一次版本升级,批量创建所有累积的待建 object store。
33
+ * 内部注册 onversionchange 确保旧连接能被浏览器主动关闭,
34
+ * blocked 事件不视为错误——旧连接释放后 success 事件会自动触发。
35
+ */
36
+ private doUpgrade;
37
+ /** 生成存储键名:`{dictType}_{locale}` */
38
+ private getStoreKey;
39
+ /** 从指定存储库读取字典缓存 */
40
+ get(storeName: string, dictType: string, locale: string): Promise<CacheEntry<DictEntry> | null>;
41
+ /** 写入字典缓存条目到指定存储库 */
42
+ set(storeName: string, dictType: string, locale: string, entry: CacheEntry<DictEntry>): Promise<void>;
43
+ /**
44
+ * 清空字典缓存数据。
45
+ * @param storeName 指定要清空的存储库名,不传则清空全部存储库
46
+ */
47
+ clear(storeName?: string): Promise<void>;
48
+ }
@@ -0,0 +1,142 @@
1
+ export const DEFAULT_STORE_NAME = "dicts";
2
+ export class IndexedDBCache {
3
+ dbName;
4
+ db = null;
5
+ version = 1;
6
+ /** 串行化升级队列,防止并发 ensureStore 导致多次版本升级 */
7
+ upgradeQueue = Promise.resolve();
8
+ /** 累积待创建的 store 名,批量创建以减少升级次数 */
9
+ pendingStores = /* @__PURE__ */ new Set();
10
+ constructor(dbName) {
11
+ this.dbName = dbName;
12
+ }
13
+ /** 初始化 IndexedDB,创建默认对象存储库 */
14
+ init() {
15
+ if (this.db) return Promise.resolve();
16
+ return new Promise((resolve, reject) => {
17
+ const request = indexedDB.open(this.dbName);
18
+ request.addEventListener("upgradeneeded", () => {
19
+ const db = request.result;
20
+ if (!db.objectStoreNames.contains(DEFAULT_STORE_NAME)) {
21
+ db.createObjectStore(DEFAULT_STORE_NAME);
22
+ }
23
+ });
24
+ request.addEventListener("success", () => {
25
+ this.db = request.result;
26
+ this.version = this.db.version;
27
+ resolve();
28
+ });
29
+ request.addEventListener("error", () => {
30
+ reject(new Error("Failed to open IndexedDB: " + (request.error?.message || "unknown error")));
31
+ });
32
+ request.addEventListener("blocked", () => {
33
+ reject(new Error("IndexedDB is blocked by another tab"));
34
+ });
35
+ });
36
+ }
37
+ /**
38
+ * 惰性确保指定名称的对象存储库存在。
39
+ * 若当前 DB 中已有该 store 则立即返回;
40
+ * 否则将 store 名加入待建集合,通过串行队列触发一次版本升级批量创建。
41
+ */
42
+ async ensureStore(storeName) {
43
+ await this.init();
44
+ if (this.db.objectStoreNames.contains(storeName)) return;
45
+ this.pendingStores.add(storeName);
46
+ this.upgradeQueue = this.upgradeQueue.then(() => this.doUpgrade());
47
+ return this.upgradeQueue;
48
+ }
49
+ /**
50
+ * 执行一次版本升级,批量创建所有累积的待建 object store。
51
+ * 内部注册 onversionchange 确保旧连接能被浏览器主动关闭,
52
+ * blocked 事件不视为错误——旧连接释放后 success 事件会自动触发。
53
+ */
54
+ async doUpgrade() {
55
+ if (this.pendingStores.size === 0) return;
56
+ const stores = new Set(this.pendingStores);
57
+ this.pendingStores.clear();
58
+ const toCreate = [...stores].filter((s) => !this.db.objectStoreNames.contains(s));
59
+ if (toCreate.length === 0) return;
60
+ this.db.onversionchange = () => {
61
+ this.db.close();
62
+ };
63
+ this.db.close();
64
+ this.version++;
65
+ await new Promise((resolve, reject) => {
66
+ const req = indexedDB.open(this.dbName, this.version);
67
+ req.addEventListener("upgradeneeded", () => {
68
+ const db = req.result;
69
+ for (const name of toCreate) {
70
+ if (!db.objectStoreNames.contains(name)) {
71
+ db.createObjectStore(name);
72
+ }
73
+ }
74
+ });
75
+ req.addEventListener("success", () => {
76
+ this.db = req.result;
77
+ resolve();
78
+ });
79
+ req.addEventListener("error", () => reject(req.error));
80
+ req.addEventListener("blocked", () => {
81
+ });
82
+ });
83
+ }
84
+ /** 生成存储键名:`{dictType}_{locale}` */
85
+ getStoreKey(dictType, locale) {
86
+ return `${dictType}_${locale}`;
87
+ }
88
+ /** 从指定存储库读取字典缓存 */
89
+ async get(storeName, dictType, locale) {
90
+ if (!this.db) return null;
91
+ await this.ensureStore(storeName);
92
+ if (!this.db) return null;
93
+ return new Promise((resolve, reject) => {
94
+ const tx = this.db.transaction(storeName, "readonly");
95
+ const store = tx.objectStore(storeName);
96
+ const request = store.get(this.getStoreKey(dictType, locale));
97
+ request.addEventListener("success", () => resolve(request.result ?? null));
98
+ request.addEventListener("error", () => reject(request.error));
99
+ });
100
+ }
101
+ /** 写入字典缓存条目到指定存储库 */
102
+ async set(storeName, dictType, locale, entry) {
103
+ if (!this.db) return;
104
+ await this.ensureStore(storeName);
105
+ if (!this.db) return;
106
+ return new Promise((resolve, reject) => {
107
+ const tx = this.db.transaction(storeName, "readwrite");
108
+ const store = tx.objectStore(storeName);
109
+ const request = store.put(entry, this.getStoreKey(dictType, locale));
110
+ request.addEventListener("success", () => resolve());
111
+ request.addEventListener("error", () => reject(request.error));
112
+ });
113
+ }
114
+ /**
115
+ * 清空字典缓存数据。
116
+ * @param storeName 指定要清空的存储库名,不传则清空全部存储库
117
+ */
118
+ async clear(storeName) {
119
+ if (!this.db) return;
120
+ if (storeName) {
121
+ if (!this.db.objectStoreNames.contains(storeName)) return;
122
+ await new Promise((resolve, reject) => {
123
+ const tx = this.db.transaction(storeName, "readwrite");
124
+ const store = tx.objectStore(storeName);
125
+ const request = store.clear();
126
+ request.addEventListener("success", () => resolve());
127
+ request.addEventListener("error", () => reject(request.error));
128
+ });
129
+ return;
130
+ }
131
+ const storeNames = Array.from(this.db.objectStoreNames);
132
+ for (const name of storeNames) {
133
+ await new Promise((resolve, reject) => {
134
+ const tx = this.db.transaction(name, "readwrite");
135
+ const store = tx.objectStore(name);
136
+ const request = store.clear();
137
+ request.addEventListener("success", () => resolve());
138
+ request.addEventListener("error", () => reject(request.error));
139
+ });
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,25 @@
1
+ import type { CacheEntry } from '../../types/index.js';
2
+ /**
3
+ * 内存缓存,支持 LRU 淘汰和 TTL 过期。
4
+ * Map 保证插入顺序,借助 keys().next() 获取最久未使用项。
5
+ */
6
+ export declare class MemoryCache<T = unknown> {
7
+ private cache;
8
+ private maxSize;
9
+ /** 毫秒级 TTL,0 表示永不过期 */
10
+ private ttl;
11
+ constructor(maxSize?: number, ttl?: number);
12
+ /** 读取缓存项,命中后移至末尾(LRU),TTL 过期则清除 */
13
+ get(key: string): CacheEntry<T> | undefined;
14
+ /** 写入缓存项,超过 maxSize 时淘汰最旧条目 */
15
+ set(key: string, entry: CacheEntry<T>): void;
16
+ has(key: string): boolean;
17
+ delete(key: string): void;
18
+ clear(): void;
19
+ /** 按 key 前缀删除缓存项。用于按仓库名清除内存缓存(key 格式: `{storeName}:...`) */
20
+ deleteByPrefix(prefix: string): void;
21
+ get size(): number;
22
+ keys(): string[];
23
+ /** 清除所有已过期的缓存项 */
24
+ private sweep;
25
+ }