@fast-china/utils 2.1.1 → 2.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/storage/index.ts"],"sourcesContent":["import { decodeSecureBase64, encodeSecureBase64 } from \"../base64/index\";\n\n/** uni-app 同步存储信息中本库实际读取的字段。 */\ninterface UniStorageInfo {\n\t/** 当前平台可见的物理键快照。 */\n\tkeys: readonly string[];\n}\n\n/** 全局 `uni` 必须提供的同步存储 API 最小结构。 */\ninterface UniStorageLike {\n\t/**\n\t * 同步读取一个物理键的原始值。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t * @returns 平台保存的值;键缺失时应返回 `undefined`、`null` 或空字符串。\n\t */\n\tgetStorageSync: (key: string) => unknown;\n\t/**\n\t * 同步读取平台当前可见的全部物理键。\n\t * @returns 至少包含只读 `keys` 数组的快照对象。\n\t */\n\tgetStorageInfoSync: () => UniStorageInfo;\n\t/**\n\t * 同步删除一个物理键;键不存在时应保持幂等。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t */\n\tremoveStorageSync: (key: string) => void;\n\t/**\n\t * 同步写入已经序列化的包络文本。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t * @param value - JSON 包络字符串,不是未经编码的业务值。\n\t */\n\tsetStorageSync: (key: string, value: string) => void;\n}\n\n/** Storage 业务值编码器。 */\nexport interface StorageCodec {\n\t/**\n\t * 把已编码文本恢复为业务值。\n\t * @param value - 由同一 Codec 的 `encode` 生成并持久化的文本。\n\t * @returns 解码后的业务值。\n\t * @throws 当文本损坏、格式不受支持或无法反序列化时应抛出错误。\n\t */\n\tdecode: (value: string) => unknown;\n\t/**\n\t * 把业务值编码为可持久化字符串。\n\t * @param value - 调用方传入的业务值。\n\t * @returns 可由同一 Codec 的 `decode` 无损恢复的文本。\n\t * @throws 当值不受支持或无法序列化时应抛出错误。\n\t */\n\tencode: (value: unknown) => string;\n}\n\n/** 程序入口调用 {@link configureStorage} 时使用的全局配置。 */\nexport interface StorageConfiguration {\n\t/** 自定义值编码器;默认使用严格 JSON Codec,同一应用生命周期内必须保持同一引用。 */\n\tcodec?: StorageCodec;\n\t/** 启用 Base64 可逆混淆;不提供加密、完整性或认证,不能与 `codec` 同时使用。 */\n\tcrypto?: boolean;\n\t/** 返回 Unix 毫秒时间戳的时钟;默认使用 `Date.now`,主要用于 TTL 测试与受控时间源。 */\n\tnow?: () => number;\n\t/** 所有物理键使用的非空命名空间前缀; */\n\tprefix?: string;\n}\n\n/** 单次 Storage 写入配置。 */\nexport interface StorageWriteOptions {\n\t/** 从写入时刻开始的有效毫秒数;必须是大于 0 的有限数,省略时永久有效。 */\n\tttlMs?: number;\n}\n\n/** `Local` 与 `Session` 的统一操作接口。 */\nexport interface StorageArea {\n\t/** 当前全局 Storage 配置的物理键前缀;首次读取会激活默认配置。 */\n\treadonly prefix: string;\n\t/**\n\t * 删除当前命名空间内的全部键,不影响同一后端中的其他应用键。\n\t * @throws `Error` 当当前平台后端不可用。\n\t */\n\tclear: () => void;\n\t/**\n\t * 获取并解码业务值;已过期记录会在读取时删除。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @returns 解码后的值;键缺失或过期时返回 `undefined`。\n\t * @throws 当键非法、包络损坏、Codec 解码失败或后端不可用时抛出错误。\n\t */\n\tget: <Value = unknown>(key: string) => Value | undefined;\n\t/**\n\t * 判断一个可成功读取且未过期的业务键是否存在。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @returns 键存在且包络有效时返回 `true`。\n\t */\n\thas: (key: string) => boolean;\n\t/**\n\t * 返回当前命名空间内的业务键快照。\n\t * @returns 已移除全局前缀并按字典序排列的新数组;不会自动清理过期项。\n\t */\n\tkeys: () => string[];\n\t/**\n\t * 扫描当前命名空间并删除全部过期记录。\n\t * @returns 本次实际删除的记录数量。\n\t * @throws 当发现损坏包络或后端不可用时抛出错误。\n\t */\n\tpruneExpired: () => number;\n\t/**\n\t * 删除单个业务键;键不存在时保持幂等。\n\t * @param key - 不含全局前缀的非空业务键。\n\t */\n\tremove: (key: string) => void;\n\t/**\n\t * 删除业务键以指定文本开头的全部条目,范围仍受全局命名空间限制。\n\t * @param keyPrefix - 不含全局前缀的非空业务键前缀。\n\t */\n\tremoveByPrefix: (keyPrefix: string) => void;\n\t/**\n\t * 编码并写入业务值,可附加惰性清理的 TTL。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @param value - 必须受当前 Codec 支持的业务值。\n\t * @param options - 可选的单次写入 TTL。\n\t * @throws 当键、TTL、业务值或后端写入无效时抛出错误。\n\t */\n\tset: <Value>(key: string, value: Value, options?: StorageWriteOptions) => void;\n}\n\n/** 浏览器 Storage 与 uni-app Storage 适配后的最小内部协议。 */\ninterface StorageBackend {\n\t/** 读取物理键原始值;缺失约定由上层统一规范为 `undefined`。 */\n\tgetItem: (key: string) => unknown;\n\t/** 枚举后端可见的全部物理键;返回值必须是不会随枚举过程变化的快照。 */\n\tkeys: () => readonly string[];\n\t/** 删除单个物理键;实现必须允许重复删除。 */\n\tremoveItem: (key: string) => void;\n\t/** 写入已经序列化的包络文本;配额和平台错误保持原样传播。 */\n\tsetItem: (key: string, value: string) => void;\n}\n\n/** 物理存储中的版本化包络;业务值始终先经 Codec 转为文本。 */\ninterface StoredEnvelope {\n\t/** Codec 编码后的业务文本;只有在包络结构校验通过后才能交给 Codec。 */\n\tdata: string;\n\t/** Unix 毫秒绝对过期时间戳;`null` 表示永久有效。 */\n\texpiresAt: number | null;\n\t/** 当前持久化协议版本;读取其他版本必须明确失败,不能猜测迁移。 */\n\tversion: 3;\n}\n\n/** 首次配置后冻结使用的解析结果和两个稳定门面。 */\ninterface ActiveStorageConfiguration {\n\t/** 首次配置后锁定的业务值 Codec 引用。 */\n\tcodec: StorageCodec;\n\t/** 已绑定 Local 后端、命名空间、Codec 与时钟的实际 Area。 */\n\tlocal: StorageArea;\n\t/** 首次配置后锁定的 TTL 时钟引用。 */\n\tnow: () => number;\n\t/** 所有 Area 共享的物理键命名空间前缀。 */\n\tprefix: string;\n\t/** 仅浏览器模式存在的 Session Area;uni-app 模式必须保持缺失。 */\n\tsession?: StorageArea;\n}\n\n/**\n * 读取并校验当前运行时的全局 uni-app 同步 Storage。\n *\n * @returns 检测到 uni-app 时返回同步 Storage;普通浏览器环境返回 `undefined`。\n * @throws `TypeError` 当全局 `uni` 存在但缺少本库需要的同步 Storage 方法。\n */\nconst getGlobalUniStorage = (): UniStorageLike | undefined => {\n\tconst value: unknown = Reflect.get(globalThis, \"uni\");\n\tif (value === undefined) return undefined;\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\tthrow new TypeError(\"全局 uni 对象未提供同步 Storage API。\");\n\t}\n\tconst storage = value as Partial<UniStorageLike>;\n\tif (\n\t\ttypeof storage.getStorageSync !== \"function\" ||\n\t\ttypeof storage.getStorageInfoSync !== \"function\" ||\n\t\ttypeof storage.removeStorageSync !== \"function\" ||\n\t\ttypeof storage.setStorageSync !== \"function\"\n\t) {\n\t\tthrow new TypeError(\"全局 uni 对象未提供同步 Storage API。\");\n\t}\n\treturn storage as UniStorageLike;\n};\n\n/** 默认 JSON Codec;显式拒绝会被 JSON.stringify 静默丢弃的顶层值。 */\nconst jsonCodec: StorageCodec = {\n\tdecode: (value): unknown => JSON.parse(value) as unknown,\n\tencode: (value): string => {\n\t\tconst encoded: unknown = JSON.stringify(value);\n\t\tif (typeof encoded !== \"string\") throw new TypeError(\"存储值无法序列化为 JSON。\");\n\t\treturn encoded;\n\t},\n};\n\n/** Base64 混淆 Codec;只隐藏明文外观,不提供加密、完整性或认证。 */\nexport const base64StorageCodec: StorageCodec = {\n\tdecode: (value): unknown => JSON.parse(decodeSecureBase64(value)) as unknown,\n\tencode: (value): string => {\n\t\tconst encoded: unknown = JSON.stringify(value);\n\t\tif (typeof encoded !== \"string\") throw new TypeError(\"存储值无法序列化为 JSON。\");\n\t\treturn encodeSecureBase64(encoded);\n\t},\n};\n\n/** 页面级唯一配置;只允许幂等重复配置,避免模块加载顺序改变行为。 */\nlet activeConfiguration: ActiveStorageConfiguration | undefined;\n\n/**\n * 判断未知值是否为非数组对象记录。\n *\n * @param value - JSON.parse 返回的未知值。\n * @returns 值为非空、非数组对象时返回 `true`。\n */\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * 校验 Storage 业务键。\n *\n * @param key - 不含全局 Prefix 的业务键或业务键前缀。\n * @throws `TypeError` 当值不是非空字符串。\n */\nconst assertKey = (key: string): void => {\n\tif (typeof key !== \"string\" || key.length === 0) throw new TypeError(\"Storage 键必须是非空字符串。\");\n};\n\n/**\n * 创建浏览器 Storage 后端。\n *\n * @remarks 平台对象在调用阶段读取,因此导入模块不会访问浏览器全局对象。\n * @param kind - 选择 `localStorage` 或 `sessionStorage`。\n * @returns 统一的内部同步后端。\n * @throws `Error` 当所选 Storage 在当前环境不可用。\n */\nconst createWebStorageBackend = (kind: \"local\" | \"session\"): StorageBackend => {\n\tconst storage = kind === \"local\" ? globalThis.localStorage : globalThis.sessionStorage;\n\tif (storage === undefined) throw new Error(`当前运行环境不支持 ${kind}Storage。`);\n\treturn {\n\t\tgetItem: (key): string | null => storage.getItem(key),\n\t\tkeys: (): string[] => {\n\t\t\tconst keys: string[] = [];\n\t\t\tfor (let index = 0; index < storage.length; index += 1) {\n\t\t\t\tconst key = storage.key(index);\n\t\t\t\tif (key !== null) keys.push(key);\n\t\t\t}\n\t\t\treturn keys;\n\t\t},\n\t\tremoveItem: (key): void => {\n\t\t\tstorage.removeItem(key);\n\t\t},\n\t\tsetItem: (key, value): void => {\n\t\t\tstorage.setItem(key, value);\n\t\t},\n\t};\n};\n\n/**\n * 把 uni-app 同步 Storage 适配为内部后端。\n *\n * @remarks uni-app 以空字符串同时表示“键缺失”和“真实空值”,因此空字符串需要结合键清单消除歧义。\n * @param storage - 已从全局 `uni` 读取并校验的同步 API。\n * @returns 统一的内部同步后端。\n */\nconst createUniStorageBackend = (storage: UniStorageLike): StorageBackend => ({\n\tgetItem: (key): unknown => {\n\t\tconst value = storage.getStorageSync(key);\n\t\tif (value !== \"\") return value;\n\t\treturn storage.getStorageInfoSync().keys.includes(key) ? value : undefined;\n\t},\n\tkeys: (): readonly string[] => [...storage.getStorageInfoSync().keys],\n\tremoveItem: (key): void => {\n\t\tstorage.removeStorageSync(key);\n\t},\n\tsetItem: (key, value): void => {\n\t\tstorage.setStorageSync(key, value);\n\t},\n});\n\n/**\n * 解析并校验版本化 Storage 包络。\n *\n * @param rawValue - 后端返回的原始值。\n * @param key - 用于错误定位的完整物理键。\n * @returns 当前 v3 包络。\n * @throws `TypeError` 当原始值不是字符串、JSON 损坏、版本不支持或字段类型非法。\n */\nconst parseStoredEnvelope = (rawValue: unknown, key: string): StoredEnvelope => {\n\tif (typeof rawValue !== \"string\") throw new TypeError(`Storage 条目“${key}”不是字符串。`);\n\ttry {\n\t\tconst parsed = JSON.parse(rawValue) as unknown;\n\t\tif (\n\t\t\t!isRecord(parsed) ||\n\t\t\tparsed[\"version\"] !== 3 ||\n\t\t\ttypeof parsed[\"data\"] !== \"string\" ||\n\t\t\t!(parsed[\"expiresAt\"] === null || (typeof parsed[\"expiresAt\"] === \"number\" && Number.isFinite(parsed[\"expiresAt\"])))\n\t\t) {\n\t\t\tthrow new TypeError(\"不支持该存储包络。\");\n\t\t}\n\t\treturn { data: parsed[\"data\"], expiresAt: parsed[\"expiresAt\"], version: 3 };\n\t} catch (cause) {\n\t\tthrow new TypeError(`Storage 条目“${key}”已损坏或不受支持。`, { cause });\n\t}\n};\n\n/**\n * 创建绑定命名空间、Codec 与时钟的 Storage Area。\n *\n * @param backendFactory - 每次操作时解析平台后端的工厂,保证导入安全并反映平台可用性。\n * @param prefix - 已校验的全局物理键前缀。\n * @param codec - 业务值与包络文本之间的 Codec。\n * @param now - TTL 计算使用的可注入时钟。\n * @returns 完整的命名空间 Storage 操作集合。\n */\nconst createStorageArea = (backendFactory: () => StorageBackend, prefix: string, codec: StorageCodec, now: () => number): StorageArea => {\n\t/**\n\t * 拼接物理键。\n\t *\n\t * @param key - 已校验业务键。\n\t * @returns 带当前命名空间前缀的物理键。\n\t */\n\tconst toStorageKey = (key: string): string => `${prefix}${key}`;\n\t/**\n\t * 枚举当前命名空间中的业务键。\n\t *\n\t * @param backend - 本次操作使用的后端。\n\t * @returns 已移除物理前缀、去重并排序的业务键。\n\t * @throws `TypeError` 当后端返回非字符串键。\n\t */\n\tconst listBusinessKeys = (backend: StorageBackend): string[] => {\n\t\tconst keys = backend.keys();\n\t\tif (!Array.isArray(keys) || !keys.every((key) => typeof key === \"string\")) {\n\t\t\tthrow new TypeError(\"Storage 后端返回的键必须是字符串。\");\n\t\t}\n\t\treturn [...new Set(keys.filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length)))].sort();\n\t};\n\t/**\n\t * 读取并处理单个包络。\n\t *\n\t * @param backend - 本次操作使用的后端。\n\t * @param key - 业务键。\n\t * @returns 未过期包络;键缺失或已经过期时返回 `undefined`。\n\t * @throws `TypeError` 当键或包络非法。\n\t * @throws `RangeError` 当注入时钟返回非有限时间戳。\n\t */\n\tconst readStoredEnvelope = (backend: StorageBackend, key: string): StoredEnvelope | undefined => {\n\t\tassertKey(key);\n\t\tconst storageKey = toStorageKey(key);\n\t\tconst rawValue = backend.getItem(storageKey);\n\t\tif (rawValue === null || rawValue === undefined) return undefined;\n\t\tconst envelope = parseStoredEnvelope(rawValue, storageKey);\n\t\tif (envelope.expiresAt === null) return envelope;\n\t\tconst timestamp = now();\n\t\tif (!Number.isFinite(timestamp)) throw new RangeError(\"Storage 时钟必须返回有限时间戳。\");\n\t\tif (timestamp < envelope.expiresAt) return envelope;\n\t\t// 过期项在读取时立即删除,后续 has/keys/pruneExpired 观察到一致状态。\n\t\tbackend.removeItem(storageKey);\n\t\treturn undefined;\n\t};\n\n\treturn {\n\t\tprefix,\n\t\tclear(): void {\n\t\t\tconst backend = backendFactory();\n\t\t\tfor (const key of listBusinessKeys(backend)) backend.removeItem(toStorageKey(key));\n\t\t},\n\t\tget<Value>(key: string): Value | undefined {\n\t\t\tconst envelope = readStoredEnvelope(backendFactory(), key);\n\t\t\tif (envelope === undefined) return undefined;\n\t\t\ttry {\n\t\t\t\treturn codec.decode(envelope.data) as Value;\n\t\t\t} catch (cause) {\n\t\t\t\tthrow new TypeError(`无法解码 Storage 条目“${toStorageKey(key)}”。`, { cause });\n\t\t\t}\n\t\t},\n\t\thas: (key): boolean => readStoredEnvelope(backendFactory(), key) !== undefined,\n\t\tkeys: (): string[] => listBusinessKeys(backendFactory()),\n\t\tpruneExpired(): number {\n\t\t\tconst backend = backendFactory();\n\t\t\tlet removed = 0;\n\t\t\tfor (const key of listBusinessKeys(backend)) {\n\t\t\t\t// read 同时处理删除;先读取一次用于区分“原本缺失”和“本轮因过期删除”。\n\t\t\t\tconst before = backend.getItem(toStorageKey(key));\n\t\t\t\tif (before !== null && before !== undefined && readStoredEnvelope(backend, key) === undefined) removed += 1;\n\t\t\t}\n\t\t\treturn removed;\n\t\t},\n\t\tremove(key: string): void {\n\t\t\tassertKey(key);\n\t\t\tbackendFactory().removeItem(toStorageKey(key));\n\t\t},\n\t\tremoveByPrefix(keyPrefix: string): void {\n\t\t\tassertKey(keyPrefix);\n\t\t\tconst backend = backendFactory();\n\t\t\tfor (const key of listBusinessKeys(backend)) if (key.startsWith(keyPrefix)) backend.removeItem(toStorageKey(key));\n\t\t},\n\t\tset<Value>(key: string, value: Value, options: StorageWriteOptions = {}): void {\n\t\t\tassertKey(key);\n\t\t\tif (value === undefined) throw new TypeError(\"不能存储顶层 `undefined`,请改为移除对应的键。\");\n\t\t\tlet expiresAt: number | null = null;\n\t\t\tif (options.ttlMs !== undefined) {\n\t\t\t\tif (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) throw new RangeError(\"`ttlMs` 必须是大于 0 的有限数。\");\n\t\t\t\tconst timestamp = now();\n\t\t\t\tif (!Number.isFinite(timestamp) || !Number.isFinite(timestamp + options.ttlMs)) {\n\t\t\t\t\tthrow new RangeError(\"Storage 过期时间超出支持的时间戳范围。\");\n\t\t\t\t}\n\t\t\t\texpiresAt = timestamp + options.ttlMs;\n\t\t\t}\n\t\t\tlet data: string;\n\t\t\ttry {\n\t\t\t\tdata = codec.encode(value);\n\t\t\t\tif (typeof data !== \"string\") throw new TypeError(\"Storage Codec 必须返回字符串。\");\n\t\t\t} catch (cause) {\n\t\t\t\tthrow new TypeError(\"无法编码存储值。\", { cause });\n\t\t\t}\n\t\t\tbackendFactory().setItem(toStorageKey(key), JSON.stringify({ data, expiresAt, version: 3 } satisfies StoredEnvelope));\n\t\t},\n\t};\n};\n\n/**\n * 获取已激活的全局 Storage 配置。\n *\n * @returns 显式配置或首次 Storage 操作创建的默认配置。\n */\nconst requireStorageConfiguration = (): ActiveStorageConfiguration => {\n\tif (activeConfiguration === undefined) configureStorage();\n\tif (activeConfiguration === undefined) throw new Error(\"无法初始化 Storage 配置。\");\n\treturn activeConfiguration;\n};\n\n/**\n * 创建稳定的公开 Storage 门面。\n *\n * @param select - 从激活配置选择 Local 或 Session 的函数。\n * @param name - 用于不可用错误的公开门面名称。\n * @returns 可安全导入、并在首次调用时解析默认或显式配置的稳定对象。\n */\nconst createStorageAreaProxy = (select: (configuration: ActiveStorageConfiguration) => StorageArea | undefined, name: string): StorageArea => {\n\t/**\n\t * 解析当前实际 Area。\n\t *\n\t * @returns 配置中的 Local 或 Session Area。\n\t * @throws `Error` 当 uni-app 模式请求 Session。\n\t */\n\tconst getArea = (): StorageArea => {\n\t\tconst area = select(requireStorageConfiguration());\n\t\tif (area === undefined) throw new Error(`uni-app 中不支持 ${name}。`);\n\t\treturn area;\n\t};\n\treturn {\n\t\tget prefix(): string {\n\t\t\treturn getArea().prefix;\n\t\t},\n\t\tclear: (): void => {\n\t\t\tgetArea().clear();\n\t\t},\n\t\tget: <Value>(key: string): Value | undefined => getArea().get<Value>(key),\n\t\thas: (key): boolean => getArea().has(key),\n\t\tkeys: (): string[] => getArea().keys(),\n\t\tpruneExpired: (): number => getArea().pruneExpired(),\n\t\tremove: (key): void => {\n\t\t\tgetArea().remove(key);\n\t\t},\n\t\tremoveByPrefix: (keyPrefix): void => {\n\t\t\tgetArea().removeByPrefix(keyPrefix);\n\t\t},\n\t\tset: <Value>(key: string, value: Value, options?: StorageWriteOptions): void => {\n\t\t\tgetArea().set(key, value, options);\n\t\t},\n\t};\n};\n\n/** 浏览器 localStorage 或自动检测的 uni-app Storage 全局业务入口。 */\nexport const Local: StorageArea = createStorageAreaProxy((configuration) => configuration.local, \"Local\");\n\n/** 浏览器 sessionStorage 的全局业务入口;uni-app 不提供会话存储。 */\nexport const Session: StorageArea = createStorageAreaProxy((configuration) => configuration.session, \"Session\");\n\n/**\n * 在首次 Storage 操作前可选配置 `Local` 与 `Session`。\n *\n * @remarks 不调用时在首次操作上使用 `fast__`、JSON Codec 与 `Date.now`。首次激活后只允许以完全相同的值和引用重复调用。若检测到\n * 全局 `uni`,则自动使用其同步 Storage 且只启用 `Local`,否则使用浏览器 `localStorage` 与 `sessionStorage`。\n * `crypto: true` 仅恢复旧版 Base64 混淆行为,不能保护敏感数据。\n * @param options - 可选的全局键前缀、Codec、旧版混淆选项与时钟。\n * @throws 配置非法、重复配置冲突或目标平台 Storage 不可用时抛出错误。\n */\nexport function configureStorage(options: StorageConfiguration = {}): void {\n\tconst prefix = options.prefix ?? \"fast__\";\n\tif (typeof prefix !== \"string\" || prefix.length === 0) {\n\t\tthrow new TypeError(\"Storage 前缀必须是非空字符串。\");\n\t}\n\tif (options.codec !== undefined && options.crypto === true) {\n\t\tthrow new TypeError(\"Storage 的 Codec 和加密选项不能同时使用。\");\n\t}\n\tconst codec = options.codec ?? (options.crypto === true ? base64StorageCodec : jsonCodec);\n\tconst now = options.now ?? Date.now;\n\tif (activeConfiguration !== undefined) {\n\t\t// 相同配置允许多个入口模块幂等调用;任何引用或值变化都视为冲突。\n\t\tif (activeConfiguration.prefix === prefix && activeConfiguration.codec === codec && activeConfiguration.now === now) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Storage 已使用其他选项完成配置。\");\n\t}\n\tconst uni = getGlobalUniStorage();\n\tconst localBackend =\n\t\tuni === undefined ? (): StorageBackend => createWebStorageBackend(\"local\") : (): StorageBackend => createUniStorageBackend(uni);\n\tconst local = createStorageArea(localBackend, prefix, codec, now);\n\tconst configuration: ActiveStorageConfiguration = { codec, local, now, prefix };\n\tif (uni === undefined) {\n\t\tconfiguration.session = createStorageArea(() => createWebStorageBackend(\"session\"), prefix, codec, now);\n\t}\n\tactiveConfiguration = configuration;\n}\n\n/** 返回全局 Storage 是否已经由应用入口配置。 */\nexport function isStorageConfigured(): boolean {\n\treturn activeConfiguration !== undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/storage/index.ts"],"sourcesContent":["import { decodeSecureBase64, encodeSecureBase64 } from \"../base64/index\";\n\n/** uni-app 同步存储信息中本库实际读取的字段。 */\ninterface UniStorageInfo {\n\t/** 当前平台可见的物理键快照。 */\n\tkeys: readonly string[];\n}\n\n/** 全局 `uni` 必须提供的同步存储 API 最小结构。 */\ninterface UniStorageLike {\n\t/**\n\t * 同步读取一个物理键的原始值。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t * @returns 平台保存的值;键缺失时应返回 `undefined`、`null` 或空字符串。\n\t */\n\tgetStorageSync: (key: string) => unknown;\n\t/**\n\t * 同步读取平台当前可见的全部物理键。\n\t * @returns 至少包含只读 `keys` 数组的快照对象。\n\t */\n\tgetStorageInfoSync: () => UniStorageInfo;\n\t/**\n\t * 同步删除一个物理键;键不存在时应保持幂等。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t */\n\tremoveStorageSync: (key: string) => void;\n\t/**\n\t * 同步写入已经序列化的包络文本。\n\t * @param key - 已包含全局命名空间前缀的物理键。\n\t * @param value - JSON 包络字符串,不是未经编码的业务值。\n\t */\n\tsetStorageSync: (key: string, value: string) => void;\n}\n\n/** Storage 业务值编码器。 */\nexport interface StorageCodec {\n\t/**\n\t * 把已编码文本恢复为业务值。\n\t * @param value - 由同一 Codec 的 `encode` 生成并持久化的文本。\n\t * @returns 解码后的业务值。\n\t * @throws 当文本损坏、格式不受支持或无法反序列化时应抛出错误。\n\t */\n\tdecode: (value: string) => unknown;\n\t/**\n\t * 把业务值编码为可持久化字符串。\n\t * @param value - 调用方传入的业务值。\n\t * @returns 可由同一 Codec 的 `decode` 无损恢复的文本。\n\t * @throws 当值不受支持或无法序列化时应抛出错误。\n\t */\n\tencode: (value: unknown) => string;\n}\n\n/** 程序入口调用 {@link configureStorage} 时使用的全局配置。 */\nexport interface StorageConfiguration {\n\t/** 自定义值编码器;默认使用严格 JSON Codec,同一应用生命周期内必须保持同一引用。 */\n\tcodec?: StorageCodec;\n\t/** 启用 Base64 可逆混淆;不提供加密、完整性或认证,不能与 `codec` 同时使用。 */\n\tcrypto?: boolean;\n\t/** 返回 Unix 毫秒时间戳的时钟;默认使用 `Date.now`,主要用于 TTL 测试与受控时间源。 */\n\tnow?: () => number;\n\t/** 所有物理键使用的非空命名空间前缀; */\n\tprefix?: string;\n}\n\n/** 单次 Storage 读取配置。 */\nexport interface StorageReadOptions {\n\t/**\n\t * 仅覆盖本次读取使用的 Codec;`true` 使用 Base64 混淆,`false` 使用 JSON,省略时使用全局配置。\n\t * 必须与写入该条目时使用的单次设置一致。\n\t */\n\tcrypto?: boolean;\n}\n\n/** 单次 Storage 写入配置。 */\nexport interface StorageWriteOptions extends StorageReadOptions {\n\t/** 从写入时刻开始的有效毫秒数;必须是大于 0 的有限数,省略时永久有效。 */\n\tttlMs?: number;\n}\n\n/** `Local` 与 `Session` 的统一操作接口。 */\nexport interface StorageArea {\n\t/** 当前全局 Storage 配置的物理键前缀;首次读取会激活默认配置。 */\n\treadonly prefix: string;\n\t/**\n\t * 删除当前命名空间内的全部键,不影响同一后端中的其他应用键。\n\t * @throws `Error` 当当前平台后端不可用。\n\t */\n\tclear: () => void;\n\t/**\n\t * 获取并解码业务值;已过期记录会在读取时删除。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @param options - 可选的单次 Base64 混淆开关;必须与写入时一致。\n\t * @returns 解码后的值;未传泛型时静态类型默认为 `string`,键缺失或过期时返回 `undefined`。\n\t * @throws 当键非法、包络损坏、Codec 解码失败或后端不可用时抛出错误。\n\t */\n\tget: <Value = string>(key: string, options?: StorageReadOptions) => Value | undefined;\n\t/**\n\t * 判断一个可成功读取且未过期的业务键是否存在。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @returns 键存在且包络有效时返回 `true`。\n\t */\n\thas: (key: string) => boolean;\n\t/**\n\t * 返回当前命名空间内的业务键快照。\n\t * @returns 已移除全局前缀并按字典序排列的新数组;不会自动清理过期项。\n\t */\n\tkeys: () => string[];\n\t/**\n\t * 扫描当前命名空间并删除全部过期记录。\n\t * @returns 本次实际删除的记录数量。\n\t * @throws 当发现损坏包络或后端不可用时抛出错误。\n\t */\n\tpruneExpired: () => number;\n\t/**\n\t * 删除单个业务键;键不存在时保持幂等。\n\t * @param key - 不含全局前缀的非空业务键。\n\t */\n\tremove: (key: string) => void;\n\t/**\n\t * 删除业务键以指定文本开头的全部条目,范围仍受全局命名空间限制。\n\t * @param keyPrefix - 不含全局前缀的非空业务键前缀。\n\t */\n\tremoveByPrefix: (keyPrefix: string) => void;\n\t/**\n\t * 编码并写入业务值,可附加惰性清理的 TTL。\n\t * @param key - 不含全局前缀的非空业务键。\n\t * @param value - 必须受当前 Codec 支持的业务值。\n\t * @param options - 可选的单次写入 TTL 与 Base64 混淆开关。\n\t * @throws 当键、TTL、业务值或后端写入无效时抛出错误。\n\t */\n\tset: <Value>(key: string, value: Value, options?: StorageWriteOptions) => void;\n}\n\n/** 浏览器 Storage 与 uni-app Storage 适配后的最小内部协议。 */\ninterface StorageBackend {\n\t/** 读取物理键原始值;缺失约定由上层统一规范为 `undefined`。 */\n\tgetItem: (key: string) => unknown;\n\t/** 枚举后端可见的全部物理键;返回值必须是不会随枚举过程变化的快照。 */\n\tkeys: () => readonly string[];\n\t/** 删除单个物理键;实现必须允许重复删除。 */\n\tremoveItem: (key: string) => void;\n\t/** 写入已经序列化的包络文本;配额和平台错误保持原样传播。 */\n\tsetItem: (key: string, value: string) => void;\n}\n\n/** 物理存储中的版本化包络;业务值始终先经 Codec 转为文本。 */\ninterface StoredEnvelope {\n\t/** Codec 编码后的业务文本;只有在包络结构校验通过后才能交给 Codec。 */\n\tdata: string;\n\t/** Unix 毫秒绝对过期时间戳;`null` 表示永久有效。 */\n\texpiresAt: number | null;\n\t/** 当前持久化协议版本;读取其他版本必须明确失败,不能猜测迁移。 */\n\tversion: 3;\n}\n\n/** 首次配置后冻结使用的解析结果和两个稳定门面。 */\ninterface ActiveStorageConfiguration {\n\t/** 首次配置后锁定的业务值 Codec 引用。 */\n\tcodec: StorageCodec;\n\t/** 已绑定 Local 后端、命名空间、Codec 与时钟的实际 Area。 */\n\tlocal: StorageArea;\n\t/** 首次配置后锁定的 TTL 时钟引用。 */\n\tnow: () => number;\n\t/** 所有 Area 共享的物理键命名空间前缀。 */\n\tprefix: string;\n\t/** 仅浏览器模式存在的 Session Area;uni-app 模式必须保持缺失。 */\n\tsession?: StorageArea;\n}\n\n/**\n * 读取并校验当前运行时的全局 uni-app 同步 Storage。\n *\n * @returns 检测到 uni-app 时返回同步 Storage;普通浏览器环境返回 `undefined`。\n * @throws `TypeError` 当全局 `uni` 存在但缺少本库需要的同步 Storage 方法。\n */\nconst getGlobalUniStorage = (): UniStorageLike | undefined => {\n\tconst value: unknown = Reflect.get(globalThis, \"uni\");\n\tif (value === undefined) return undefined;\n\tif ((typeof value !== \"object\" && typeof value !== \"function\") || value === null) {\n\t\tthrow new TypeError(\"全局 uni 对象未提供同步 Storage API。\");\n\t}\n\tconst storage = value as Partial<UniStorageLike>;\n\tif (\n\t\ttypeof storage.getStorageSync !== \"function\" ||\n\t\ttypeof storage.getStorageInfoSync !== \"function\" ||\n\t\ttypeof storage.removeStorageSync !== \"function\" ||\n\t\ttypeof storage.setStorageSync !== \"function\"\n\t) {\n\t\tthrow new TypeError(\"全局 uni 对象未提供同步 Storage API。\");\n\t}\n\treturn storage as UniStorageLike;\n};\n\n/** 默认 JSON Codec;显式拒绝会被 JSON.stringify 静默丢弃的顶层值。 */\nconst jsonCodec: StorageCodec = {\n\tdecode: (value): unknown => JSON.parse(value) as unknown,\n\tencode: (value): string => {\n\t\tconst encoded: unknown = JSON.stringify(value);\n\t\tif (typeof encoded !== \"string\") throw new TypeError(\"存储值无法序列化为 JSON。\");\n\t\treturn encoded;\n\t},\n};\n\n/** Base64 混淆 Codec;只隐藏明文外观,不提供加密、完整性或认证。 */\nexport const base64StorageCodec: StorageCodec = {\n\tdecode: (value): unknown => decodeSecureBase64(value).parseJson(),\n\tencode: (value): string => {\n\t\tconst encoded: unknown = JSON.stringify(value);\n\t\tif (typeof encoded !== \"string\") throw new TypeError(\"存储值无法序列化为 JSON。\");\n\t\treturn encodeSecureBase64(encoded);\n\t},\n};\n\n/** 页面级唯一配置;只允许幂等重复配置,避免模块加载顺序改变行为。 */\nlet activeConfiguration: ActiveStorageConfiguration | undefined;\n\n/**\n * 判断未知值是否为非数组对象记录。\n *\n * @param value - JSON.parse 返回的未知值。\n * @returns 值为非空、非数组对象时返回 `true`。\n */\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * 校验 Storage 业务键。\n *\n * @param key - 不含全局 Prefix 的业务键或业务键前缀。\n * @throws `TypeError` 当值不是非空字符串。\n */\nconst assertKey = (key: string): void => {\n\tif (typeof key !== \"string\" || key.length === 0) throw new TypeError(\"Storage 键必须是非空字符串。\");\n};\n\n/**\n * 创建浏览器 Storage 后端。\n *\n * @remarks 平台对象在调用阶段读取,因此导入模块不会访问浏览器全局对象。\n * @param kind - 选择 `localStorage` 或 `sessionStorage`。\n * @returns 统一的内部同步后端。\n * @throws `Error` 当所选 Storage 在当前环境不可用。\n */\nconst createWebStorageBackend = (kind: \"local\" | \"session\"): StorageBackend => {\n\tconst storage = kind === \"local\" ? globalThis.localStorage : globalThis.sessionStorage;\n\tif (storage === undefined) throw new Error(`当前运行环境不支持 ${kind}Storage。`);\n\treturn {\n\t\tgetItem: (key): string | null => storage.getItem(key),\n\t\tkeys: (): string[] => {\n\t\t\tconst keys: string[] = [];\n\t\t\tfor (let index = 0; index < storage.length; index += 1) {\n\t\t\t\tconst key = storage.key(index);\n\t\t\t\tif (key !== null) keys.push(key);\n\t\t\t}\n\t\t\treturn keys;\n\t\t},\n\t\tremoveItem: (key): void => {\n\t\t\tstorage.removeItem(key);\n\t\t},\n\t\tsetItem: (key, value): void => {\n\t\t\tstorage.setItem(key, value);\n\t\t},\n\t};\n};\n\n/**\n * 把 uni-app 同步 Storage 适配为内部后端。\n *\n * @remarks uni-app 以空字符串同时表示“键缺失”和“真实空值”,因此空字符串需要结合键清单消除歧义。\n * @param storage - 已从全局 `uni` 读取并校验的同步 API。\n * @returns 统一的内部同步后端。\n */\nconst createUniStorageBackend = (storage: UniStorageLike): StorageBackend => ({\n\tgetItem: (key): unknown => {\n\t\tconst value = storage.getStorageSync(key);\n\t\tif (value !== \"\") return value;\n\t\treturn storage.getStorageInfoSync().keys.includes(key) ? value : undefined;\n\t},\n\tkeys: (): readonly string[] => [...storage.getStorageInfoSync().keys],\n\tremoveItem: (key): void => {\n\t\tstorage.removeStorageSync(key);\n\t},\n\tsetItem: (key, value): void => {\n\t\tstorage.setStorageSync(key, value);\n\t},\n});\n\n/**\n * 解析并校验版本化 Storage 包络。\n *\n * @param rawValue - 后端返回的原始值。\n * @param key - 用于错误定位的完整物理键。\n * @returns 当前 v3 包络。\n * @throws `TypeError` 当原始值不是字符串、JSON 损坏、版本不支持或字段类型非法。\n */\nconst parseStoredEnvelope = (rawValue: unknown, key: string): StoredEnvelope => {\n\tif (typeof rawValue !== \"string\") throw new TypeError(`Storage 条目“${key}”不是字符串。`);\n\ttry {\n\t\tconst parsed = JSON.parse(rawValue) as unknown;\n\t\tif (\n\t\t\t!isRecord(parsed) ||\n\t\t\tparsed[\"version\"] !== 3 ||\n\t\t\ttypeof parsed[\"data\"] !== \"string\" ||\n\t\t\t!(parsed[\"expiresAt\"] === null || (typeof parsed[\"expiresAt\"] === \"number\" && Number.isFinite(parsed[\"expiresAt\"])))\n\t\t) {\n\t\t\tthrow new TypeError(\"不支持该存储包络。\");\n\t\t}\n\t\treturn { data: parsed[\"data\"], expiresAt: parsed[\"expiresAt\"], version: 3 };\n\t} catch (cause) {\n\t\tthrow new TypeError(`Storage 条目“${key}”已损坏或不受支持。`, { cause });\n\t}\n};\n\n/**\n * 创建绑定命名空间、Codec 与时钟的 Storage Area。\n *\n * @param backendFactory - 每次操作时解析平台后端的工厂,保证导入安全并反映平台可用性。\n * @param prefix - 已校验的全局物理键前缀。\n * @param codec - 业务值与包络文本之间的 Codec。\n * @param now - TTL 计算使用的可注入时钟。\n * @returns 完整的命名空间 Storage 操作集合。\n */\nconst createStorageArea = (backendFactory: () => StorageBackend, prefix: string, codec: StorageCodec, now: () => number): StorageArea => {\n\t/**\n\t * 拼接物理键。\n\t *\n\t * @param key - 已校验业务键。\n\t * @returns 带当前命名空间前缀的物理键。\n\t */\n\tconst toStorageKey = (key: string): string => `${prefix}${key}`;\n\t/**\n\t * 解析本次读写实际使用的 Codec。\n\t *\n\t * @param crypto - 单次 Base64 混淆开关;省略时沿用 Area 全局 Codec。\n\t * @returns 本次操作使用的全局、JSON 或 Base64 Codec。\n\t * @throws `TypeError` 当 JavaScript 调用方传入非布尔值。\n\t */\n\tconst resolveOperationCodec = (crypto: boolean | undefined): StorageCodec => {\n\t\tif (crypto === undefined) return codec;\n\t\tif (typeof crypto !== \"boolean\") throw new TypeError(\"Storage 单次 `crypto` 选项必须是布尔值。\");\n\t\treturn crypto ? base64StorageCodec : jsonCodec;\n\t};\n\t/**\n\t * 枚举当前命名空间中的业务键。\n\t *\n\t * @param backend - 本次操作使用的后端。\n\t * @returns 已移除物理前缀、去重并排序的业务键。\n\t * @throws `TypeError` 当后端返回非字符串键。\n\t */\n\tconst listBusinessKeys = (backend: StorageBackend): string[] => {\n\t\tconst keys = backend.keys();\n\t\tif (!Array.isArray(keys) || !keys.every((key) => typeof key === \"string\")) {\n\t\t\tthrow new TypeError(\"Storage 后端返回的键必须是字符串。\");\n\t\t}\n\t\treturn [...new Set(keys.filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length)))].sort();\n\t};\n\t/**\n\t * 读取并处理单个包络。\n\t *\n\t * @param backend - 本次操作使用的后端。\n\t * @param key - 业务键。\n\t * @returns 未过期包络;键缺失或已经过期时返回 `undefined`。\n\t * @throws `TypeError` 当键或包络非法。\n\t * @throws `RangeError` 当注入时钟返回非有限时间戳。\n\t */\n\tconst readStoredEnvelope = (backend: StorageBackend, key: string): StoredEnvelope | undefined => {\n\t\tassertKey(key);\n\t\tconst storageKey = toStorageKey(key);\n\t\tconst rawValue = backend.getItem(storageKey);\n\t\tif (rawValue === null || rawValue === undefined) return undefined;\n\t\tconst envelope = parseStoredEnvelope(rawValue, storageKey);\n\t\tif (envelope.expiresAt === null) return envelope;\n\t\tconst timestamp = now();\n\t\tif (!Number.isFinite(timestamp)) throw new RangeError(\"Storage 时钟必须返回有限时间戳。\");\n\t\tif (timestamp < envelope.expiresAt) return envelope;\n\t\t// 过期项在读取时立即删除,后续 has/keys/pruneExpired 观察到一致状态。\n\t\tbackend.removeItem(storageKey);\n\t\treturn undefined;\n\t};\n\n\treturn {\n\t\tprefix,\n\t\tclear(): void {\n\t\t\tconst backend = backendFactory();\n\t\t\tfor (const key of listBusinessKeys(backend)) backend.removeItem(toStorageKey(key));\n\t\t},\n\t\tget<Value>(key: string, options: StorageReadOptions = {}): Value | undefined {\n\t\t\tconst envelope = readStoredEnvelope(backendFactory(), key);\n\t\t\tif (envelope === undefined) return undefined;\n\t\t\ttry {\n\t\t\t\treturn resolveOperationCodec(options.crypto).decode(envelope.data) as Value;\n\t\t\t} catch (cause) {\n\t\t\t\tthrow new TypeError(`无法解码 Storage 条目“${toStorageKey(key)}”。`, { cause });\n\t\t\t}\n\t\t},\n\t\thas: (key): boolean => readStoredEnvelope(backendFactory(), key) !== undefined,\n\t\tkeys: (): string[] => listBusinessKeys(backendFactory()),\n\t\tpruneExpired(): number {\n\t\t\tconst backend = backendFactory();\n\t\t\tlet removed = 0;\n\t\t\tfor (const key of listBusinessKeys(backend)) {\n\t\t\t\t// read 同时处理删除;先读取一次用于区分“原本缺失”和“本轮因过期删除”。\n\t\t\t\tconst before = backend.getItem(toStorageKey(key));\n\t\t\t\tif (before !== null && before !== undefined && readStoredEnvelope(backend, key) === undefined) removed += 1;\n\t\t\t}\n\t\t\treturn removed;\n\t\t},\n\t\tremove(key: string): void {\n\t\t\tassertKey(key);\n\t\t\tbackendFactory().removeItem(toStorageKey(key));\n\t\t},\n\t\tremoveByPrefix(keyPrefix: string): void {\n\t\t\tassertKey(keyPrefix);\n\t\t\tconst backend = backendFactory();\n\t\t\tfor (const key of listBusinessKeys(backend)) if (key.startsWith(keyPrefix)) backend.removeItem(toStorageKey(key));\n\t\t},\n\t\tset<Value>(key: string, value: Value, options: StorageWriteOptions = {}): void {\n\t\t\tassertKey(key);\n\t\t\tif (value === undefined) throw new TypeError(\"不能存储顶层 `undefined`,请改为移除对应的键。\");\n\t\t\tlet expiresAt: number | null = null;\n\t\t\tif (options.ttlMs !== undefined) {\n\t\t\t\tif (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) throw new RangeError(\"`ttlMs` 必须是大于 0 的有限数。\");\n\t\t\t\tconst timestamp = now();\n\t\t\t\tif (!Number.isFinite(timestamp) || !Number.isFinite(timestamp + options.ttlMs)) {\n\t\t\t\t\tthrow new RangeError(\"Storage 过期时间超出支持的时间戳范围。\");\n\t\t\t\t}\n\t\t\t\texpiresAt = timestamp + options.ttlMs;\n\t\t\t}\n\t\t\tlet data: string;\n\t\t\ttry {\n\t\t\t\tdata = resolveOperationCodec(options.crypto).encode(value);\n\t\t\t\tif (typeof data !== \"string\") throw new TypeError(\"Storage Codec 必须返回字符串。\");\n\t\t\t} catch (cause) {\n\t\t\t\tthrow new TypeError(\"无法编码存储值。\", { cause });\n\t\t\t}\n\t\t\tbackendFactory().setItem(toStorageKey(key), JSON.stringify({ data, expiresAt, version: 3 } satisfies StoredEnvelope));\n\t\t},\n\t};\n};\n\n/**\n * 获取已激活的全局 Storage 配置。\n *\n * @returns 显式配置或首次 Storage 操作创建的默认配置。\n */\nconst requireStorageConfiguration = (): ActiveStorageConfiguration => {\n\tif (activeConfiguration === undefined) configureStorage();\n\tif (activeConfiguration === undefined) throw new Error(\"无法初始化 Storage 配置。\");\n\treturn activeConfiguration;\n};\n\n/**\n * 创建稳定的公开 Storage 门面。\n *\n * @param select - 从激活配置选择 Local 或 Session 的函数。\n * @param name - 用于不可用错误的公开门面名称。\n * @returns 可安全导入、并在首次调用时解析默认或显式配置的稳定对象。\n */\nconst createStorageAreaProxy = (select: (configuration: ActiveStorageConfiguration) => StorageArea | undefined, name: string): StorageArea => {\n\t/**\n\t * 解析当前实际 Area。\n\t *\n\t * @returns 配置中的 Local 或 Session Area。\n\t * @throws `Error` 当 uni-app 模式请求 Session。\n\t */\n\tconst getArea = (): StorageArea => {\n\t\tconst area = select(requireStorageConfiguration());\n\t\tif (area === undefined) throw new Error(`uni-app 中不支持 ${name}。`);\n\t\treturn area;\n\t};\n\treturn {\n\t\tget prefix(): string {\n\t\t\treturn getArea().prefix;\n\t\t},\n\t\tclear: (): void => {\n\t\t\tgetArea().clear();\n\t\t},\n\t\tget: <Value>(key: string, options?: StorageReadOptions): Value | undefined => getArea().get<Value>(key, options),\n\t\thas: (key): boolean => getArea().has(key),\n\t\tkeys: (): string[] => getArea().keys(),\n\t\tpruneExpired: (): number => getArea().pruneExpired(),\n\t\tremove: (key): void => {\n\t\t\tgetArea().remove(key);\n\t\t},\n\t\tremoveByPrefix: (keyPrefix): void => {\n\t\t\tgetArea().removeByPrefix(keyPrefix);\n\t\t},\n\t\tset: <Value>(key: string, value: Value, options?: StorageWriteOptions): void => {\n\t\t\tgetArea().set(key, value, options);\n\t\t},\n\t};\n};\n\n/** 浏览器 localStorage 或自动检测的 uni-app Storage 全局业务入口。 */\nexport const Local: StorageArea = createStorageAreaProxy((configuration) => configuration.local, \"Local\");\n\n/** 浏览器 sessionStorage 的全局业务入口;uni-app 不提供会话存储。 */\nexport const Session: StorageArea = createStorageAreaProxy((configuration) => configuration.session, \"Session\");\n\n/**\n * 在首次 Storage 操作前可选配置 `Local` 与 `Session`。\n *\n * @remarks 不调用时在首次操作上使用 `fast__`、JSON Codec 与 `Date.now`。首次激活后只允许以完全相同的值和引用重复调用。若检测到\n * 全局 `uni`,则自动使用其同步 Storage 且只启用 `Local`,否则使用浏览器 `localStorage` 与 `sessionStorage`。\n * `crypto: true` 仅恢复旧版 Base64 混淆行为,不能保护敏感数据。\n * @param options - 可选的全局键前缀、Codec、旧版混淆选项与时钟。\n * @throws 配置非法、重复配置冲突或目标平台 Storage 不可用时抛出错误。\n */\nexport function configureStorage(options: StorageConfiguration = {}): void {\n\tconst prefix = options.prefix ?? \"fast__\";\n\tif (typeof prefix !== \"string\" || prefix.length === 0) {\n\t\tthrow new TypeError(\"Storage 前缀必须是非空字符串。\");\n\t}\n\tif (options.codec !== undefined && options.crypto === true) {\n\t\tthrow new TypeError(\"Storage 的 Codec 和加密选项不能同时使用。\");\n\t}\n\tconst codec = options.codec ?? (options.crypto === true ? base64StorageCodec : jsonCodec);\n\tconst now = options.now ?? Date.now;\n\tif (activeConfiguration !== undefined) {\n\t\t// 相同配置允许多个入口模块幂等调用;任何引用或值变化都视为冲突。\n\t\tif (activeConfiguration.prefix === prefix && activeConfiguration.codec === codec && activeConfiguration.now === now) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Storage 已使用其他选项完成配置。\");\n\t}\n\tconst uni = getGlobalUniStorage();\n\tconst localBackend =\n\t\tuni === undefined ? (): StorageBackend => createWebStorageBackend(\"local\") : (): StorageBackend => createUniStorageBackend(uni);\n\tconst local = createStorageArea(localBackend, prefix, codec, now);\n\tconst configuration: ActiveStorageConfiguration = { codec, local, now, prefix };\n\tif (uni === undefined) {\n\t\tconfiguration.session = createStorageArea(() => createWebStorageBackend(\"session\"), prefix, codec, now);\n\t}\n\tactiveConfiguration = configuration;\n}\n\n/** 返回全局 Storage 是否已经由应用入口配置。 */\nexport function isStorageConfigured(): boolean {\n\treturn activeConfiguration !== undefined;\n}\n"],"mappings":"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"}
package/docs/API.md CHANGED
@@ -21,10 +21,15 @@ import { Local, Session } from "@fast-china/utils";
21
21
 
22
22
  Local.set("profile", { name: "Ada" }, { ttlMs: 3_600_000 });
23
23
  Session.set("draft", { step: 2 });
24
+
25
+ Local.set("private-profile", { name: "Ada" }, { crypto: true });
26
+ Local.get<{ name: string }>("private-profile", { crypto: true });
24
27
  ```
25
28
 
26
29
  `Local` and `Session` provide `get`, `set`, `has`, `remove`, `removeByPrefix`, `keys`, `pruneExpired`, and namespace-scoped `clear`. Missing and expired values return `undefined`. Invalid TTL values, empty prefixes, malformed stored envelopes, unavailable platform storage, and conflicting repeated configuration throw errors. Native storage quota and privacy errors are propagated. Custom options must be configured before the first Storage operation.
27
30
 
31
+ `get<Value = string>()` has the static return type `string | undefined` when its generic is omitted, so string entries can be read directly. The codec still restores the original JSON value at runtime and does not convert objects, arrays, or other non-string values to strings; pass an explicit generic when accurate type information is required.
32
+
28
33
  For uni-app, the first Storage operation or an explicit `configureStorage` call detects the global `uni` object and uses its synchronous Storage API. uni-app has no separate session backend, so `Session` throws when called in this mode.
29
34
 
30
35
  ```ts
@@ -33,7 +38,11 @@ import { Local } from "@fast-china/utils";
33
38
  Local.set("token", "value");
34
39
  ```
35
40
 
36
- `configureStorage({ prefix: "admin:", crypto: true })` restores the old global prefix and Base64-obfuscation options. `crypto: true` and `base64StorageCodec` are reversible encoding rather than encryption and must not protect secrets. A custom `codec` may be supplied instead of `crypto`.
41
+ `configureStorage({ prefix: "admin:", crypto: true })` restores the old global prefix and Base64-obfuscation options. `Local` and `Session` `set/get` also accept a per-operation `{ crypto: boolean }`: `true` selects the Base64 codec, `false` selects the JSON codec, and omission uses the global codec. An operation override does not mutate global configuration. The current v3 envelope does not record its codec, so writes and reads of the same entry must use matching options; a mismatch throws a decoding error.
42
+
43
+ `crypto: true` and `base64StorageCodec` are reversible encoding rather than encryption and must not protect secrets. A custom `codec` may be supplied instead of global `crypto`.
44
+
45
+ `decodeBase64`, `decodeBase64Url`, `decodeLatin1Base64`, and `decodeSecureBase64` return the primitive-string `DecodedText` type. It is directly assignable to `string` and supports strict equality; JSON is returned only through an explicit `.parseJson<T = any>()` call, and the library never infers JSON from text content. The first text decode lazily installs a non-enumerable `String.prototype.parseJson`; a foreign property with the same name causes a `TypeError` instead of being overwritten. The generic type describes the expected shape but does not perform runtime validation.
37
46
 
38
47
  `encodeSecureBase64` and `decodeSecureBase64` preserve the legacy dictionary payload. The random prefix prefers Web Crypto and falls back to `Math.random()` when unavailable; it does not provide a security property. Given the same default six-character prefix, valid legacy payloads remain byte-for-byte compatible. The old dictionary references an unavailable character for Base64 lengths 101–124, so the current implementation inserts a one-character fallback that the legacy removal flow can decode. The old custom-length argument always generated six random characters; the current API correctly generates `prefixLength` characters. Custom lengths must match during encoding and decoding; `0` disables both the prefix and dictionary insertion. The format remains reversible encoding rather than encryption.
39
48
 
@@ -55,16 +64,25 @@ The identifier is an installation-scoped value, not a hardware identifier, authe
55
64
 
56
65
  ## Logger
57
66
 
58
- Logger scope belongs to each message rather than a mutable logger or child instance:
67
+ Logger scope belongs to each entry rather than a logger or child instance. The default `logger` works without construction and has a minimum level of `debug`. Configure it once
68
+ at application startup when uni-app App-Plus needs split object output:
59
69
 
60
70
  ```ts
61
- import { logger } from "@fast-china/utils";
71
+ import { configureLogger, logger } from "@fast-china/utils";
62
72
 
63
- logger.info("storage", "profile loaded", { userId: 1 });
73
+ configureLogger({ uniAppPlusSplit: true });
74
+ logger.log("Launch", { code: 200, data: { id: 1 } });
75
+ logger.log("storage", "profile loaded", { userId: 1 });
64
76
  logger.error("network", "request failed", error);
65
77
  ```
66
78
 
67
- `createLogger` configures the minimum level, brand prefix, sink, and optional uni-app App-Plus split output. Scope must be a non-empty string without surrounding whitespace.
79
+ Log content is optional and may directly contain objects, arrays, `Error` instances, or other values. Non-string values are passed unchanged to
80
+ the Sink in normal runtimes. With App-Plus splitting enabled, the heading is emitted separately and each additional value is converted to readable
81
+ text because HBuilderX does not reliably display objects.
82
+
83
+ `configureLogger` replaces the complete configuration of the default `logger`; previously retained `logger` references immediately observe the new
84
+ configuration. Calling it without options restores all defaults. `createLogger` creates an isolated instance unaffected by global configuration.
85
+ Logger's `debug`, `log`, `warn`, and `error` methods call the matching Sink methods; the default Sink maps them to `console.debug`, `console.log`, `console.warn`, and `console.error`. Both support a minimum level, brand prefix, Sink, and optional App-Plus split output. Scope must be a non-empty string without surrounding whitespace.
68
86
 
69
87
  ## Clipboard
70
88
 
@@ -93,13 +111,21 @@ The TypeScript Crypto public API mirrors the public methods and algorithm casing
93
111
 
94
112
  The Base64 v1 payload produced by `AESEncryptAuthenticated`, the `FAST-AES-256-GCM-V1` password payload, PBKDF2 password hashes, and PKCS#8/SPKI PEM keys interoperate with .NET in both directions. MD5 and HMAC output lowercase hexadecimal; SHA-1/256/384/512 output uppercase hexadecimal, matching .NET.
95
113
 
114
+ `AESDecrypt`, `AESDecryptAuthenticated`, `AESDecryptWithPassword`, and `RSADecryptOAEP` return the primitive-string `DecodedText` type (wrapped in a Promise for asynchronous APIs). Use the result directly as plaintext or call `.parseJson<T = any>()` explicitly:
115
+
116
+ ```ts
117
+ const plaintext = await AESDecryptWithPassword(payload, password);
118
+ const raw: string = plaintext;
119
+ const result = plaintext.parseJson<{ id: number }>();
120
+ ```
121
+
96
122
  Store passwords with `HashPasswordPBKDF2SHA256` and `VerifyPasswordPBKDF2SHA256`; the result is not decryptable. AES-GCM provides confidentiality and integrity, HMAC authenticates with a shared key, SHA-2 computes digests, and HKDF/PBKDF2 derive keys. MD5, SHA-1, AES-CBC, and AES-ECB do not provide modern password-storage or authenticated-encryption guarantees.
97
123
 
98
124
  ## Modules
99
125
 
100
126
  - `array`: `chunk`, `removeNullishValues`, `unique`, `uniqueBy`, `groupBy`, `partition`, `difference`, `intersection`, `hasDuplicatesBy`, and `allEqualBy`.
101
127
  - `async`: abort-aware `sleep`, timeout, retry, bounded concurrent mapping, debounce, and throttle primitives.
102
- - `base64`: strict UTF-8 Base64/Base64URL byte and text functions plus the historical Latin-1 and dictionary-obfuscation functions.
128
+ - `base64`: strict UTF-8 Base64/Base64URL byte functions and chainable text results plus the historical Latin-1 and dictionary-obfuscation functions.
103
129
  - `color`: Hex parsing/formatting/mixing, explicit black/white mixing, luminance, and contrast helpers.
104
130
  - `crypto`: random bytes, digests, HMAC, PBKDF2, HKDF, AES, RSA-OAEP/PSS, ECDSA, and ECDH.
105
131
  - `date`: date validation and arithmetic, day ranges, relative formatting, and the seven historical date helpers as named functions.
package/docs/API.zh-CN.md CHANGED
@@ -21,10 +21,15 @@ import { Local, Session } from "@fast-china/utils";
21
21
 
22
22
  Local.set("profile", { name: "Ada" }, { ttlMs: 3_600_000 });
23
23
  Session.set("draft", { step: 2 });
24
+
25
+ Local.set("private-profile", { name: "Ada" }, { crypto: true });
26
+ Local.get<{ name: string }>("private-profile", { crypto: true });
24
27
  ```
25
28
 
26
29
  `Local` 和 `Session` 提供 `get`、`set`、`has`、`remove`、`removeByPrefix`、`keys`、`pruneExpired` 和仅清理当前命名空间的 `clear`。键缺失或过期时返回 `undefined`。TTL 非法、Prefix 为空、存储包络损坏、平台 Storage 不可用或重复配置发生冲突时抛出错误;浏览器配额与隐私策略错误直接向上传播。自定义选项必须在首次 Storage 操作前配置。
27
30
 
31
+ `get<Value = string>()` 在未传泛型时的静态返回类型为 `string | undefined`,可以直接读取字符串条目。Codec 在运行时仍通过 JSON 反序列化恢复原值,因此对象、数组或其他非字符串值不会被转换成字符串;需要准确类型提示时显式传入对应泛型。
32
+
28
33
  uni-app 中,首次 Storage 操作或显式调用 `configureStorage` 会自动检测全局 `uni` 并使用其同步 Storage API。uni-app 没有独立 Session 后端,因此该模式调用 `Session` 会明确抛错。
29
34
 
30
35
  ```ts
@@ -33,7 +38,11 @@ import { Local } from "@fast-china/utils";
33
38
  Local.set("token", "value");
34
39
  ```
35
40
 
36
- `configureStorage({ prefix: "admin:", crypto: true })` 恢复了旧版全局前缀与 Base64 混淆选项。`crypto: true` `base64StorageCodec` 都只是可逆编码,不是加密,不能保护敏感数据。可以使用自定义 `codec` 替代 `crypto`。
41
+ `configureStorage({ prefix: "admin:", crypto: true })` 恢复了旧版全局前缀与 Base64 混淆选项。`Local` `Session` `set/get` 也接受单次 `{ crypto: boolean }`:`true` 使用 Base64 Codec,`false` 使用 JSON Codec,省略时沿用全局 Codec。单次设置不会修改全局配置;当前 v3 包络不记录 Codec,读写同一条目时必须传入一致选项,错误配置会明确抛出解码错误。
42
+
43
+ `crypto: true` 和 `base64StorageCodec` 都只是可逆编码,不是加密,不能保护敏感数据。可以使用自定义 `codec` 替代全局 `crypto`。
44
+
45
+ `decodeBase64`、`decodeBase64Url`、`decodeLatin1Base64` 与 `decodeSecureBase64` 返回原始字符串类型 `DecodedText`,可以直接赋值给 `string` 或参与严格比较;显式调用 `.parseJson<T = any>()` 才返回 JSON 值,库不会根据文本内容自动推断 JSON。首次文本解码会按需安装不可枚举的 `String.prototype.parseJson`;若同名属性已被其他实现占用则抛出 `TypeError`,不会覆盖。泛型只描述期望类型,不执行运行时结构校验。
37
46
 
38
47
  `encodeSecureBase64` 与 `decodeSecureBase64` 保留旧字典兼容载荷。随机前缀优先使用 Web Crypto,能力缺失时回退到 `Math.random()`;它不承担安全用途。给定相同的默认 6 字符前缀时,有效旧载荷保持逐字符兼容;旧字典在 Base64 长度 101–124 时会引用越界,当前实现使用单字符回退,旧删除字典流程仍可解码。旧自定义长度参数始终生成 6 个随机字符,当前 API 已按 `prefixLength` 正确生成。自定义 `prefixLength` 必须在编码和解码时保持一致;传入 `0` 会同时关闭随机前缀与字典插入。该格式仍是可逆编码,不等同于加密。
39
48
 
@@ -55,16 +64,24 @@ installationIdentity.clear();
55
64
 
56
65
  ## Logger
57
66
 
58
- Logger 作用域属于每条日志,不保存在可变 Logger 或 Child 实例中:
67
+ Logger 作用域属于每条日志,不保存在 Logger 或 Child 实例中。默认 `logger` 无需创建即可使用,最低输出级别为 `debug`;uni-app App-Plus
68
+ 需要拆分对象输出时,在应用入口配置一次:
59
69
 
60
70
  ```ts
61
- import { logger } from "@fast-china/utils";
71
+ import { configureLogger, logger } from "@fast-china/utils";
62
72
 
63
- logger.info("storage", "profile loaded", { userId: 1 });
73
+ configureLogger({ uniAppPlusSplit: true });
74
+ logger.log("Launch", { code: 200, data: { id: 1 } });
75
+ logger.log("storage", "profile loaded", { userId: 1 });
64
76
  logger.error("network", "request failed", error);
65
77
  ```
66
78
 
67
- `createLogger` 只配置最低级别、品牌前缀、Sink 和可选的 uni-app App-Plus 拆分输出。作用域必须是无外围空白的非空字符串。
79
+ 日志内容可省略,也可以直接传入对象、数组、`Error` 等任意值。普通环境会把非字符串值原样传给 Sink;启用 App-Plus
80
+ 拆分输出后,为解决 HBuilderX 无法正确显示对象的问题,标题单独输出,附加值逐条转换为可读文本。
81
+
82
+ `configureLogger` 会替换默认 `logger` 的完整配置,已经保存的 `logger` 引用也会立即使用新配置;无参数调用会恢复默认值。
83
+ `createLogger` 用于创建不受全局配置影响的独立实例。Logger 的 `debug`、`log`、`warn`、`error` 分别调用 Sink 的同名方法,默认 Sink 对应 `console.debug`、`console.log`、`console.warn`、`console.error`。两者均支持最低级别、品牌前缀、Sink 和可选的 App-Plus 拆分输出。
84
+ 作用域必须是无外围空白的非空字符串。
68
85
 
69
86
  ## 剪贴板
70
87
 
@@ -93,13 +110,21 @@ TypeScript Crypto 公共 API 与 .NET `CryptoUtil` 的公开方法及算法名
93
110
 
94
111
  `AESEncryptAuthenticated` 的 Base64 v1 载荷、`AESEncryptWithPassword` 的 `FAST-AES-256-GCM-V1` 载荷、PBKDF2 密码哈希以及 PKCS#8/SPKI PEM 密钥均可与 .NET 双向使用。MD5 与 HMAC 输出小写十六进制;SHA-1/256/384/512 输出大写十六进制,与 .NET 保持一致。
95
112
 
113
+ `AESDecrypt`、`AESDecryptAuthenticated`、`AESDecryptWithPassword` 与 `RSADecryptOAEP` 返回原始字符串类型 `DecodedText`(异步入口返回其 Promise)。返回值可直接作为明文字符串使用,也可通过 `.parseJson<T = any>()` 显式解析 JSON:
114
+
115
+ ```ts
116
+ const plaintext = await AESDecryptWithPassword(payload, password);
117
+ const raw: string = plaintext;
118
+ const result = plaintext.parseJson<{ id: number }>();
119
+ ```
120
+
96
121
  密码存储使用 `HashPasswordPBKDF2SHA256` 和 `VerifyPasswordPBKDF2SHA256`;该哈希不可解密。需要同时保证机密性和完整性的文本使用 AES-GCM 入口。HMAC 用于共享密钥认证,SHA-2 用于摘要,HKDF/PBKDF2 用于密钥派生。MD5、SHA-1、AES-CBC 和 AES-ECB 不提供现代密码存储或认证加密保证。
97
122
 
98
123
  ## 模块
99
124
 
100
125
  - `array`:分块、压缩、去重、分组、分区、差集、交集和一致性判断。
101
126
  - `async`:支持取消的 Sleep、超时、重试、受限并发映射、防抖和节流。
102
- - `base64`:严格 UTF-8 Base64/Base64URL 字节与文本函数,以及 Latin-1 和 SecureBase64 兼容函数。
127
+ - `base64`:严格 UTF-8 Base64/Base64URL 字节与链式文本结果,以及 Latin-1 和 SecureBase64 兼容函数。
103
128
  - `color`:颜色解析、格式化、混合、明暗、亮度和对比度。
104
129
  - `crypto`:随机字节、摘要、HMAC、PBKDF2、HKDF、AES、RSA-OAEP/PSS、ECDSA 和 ECDH。
105
130
  - `date`:日期校验、加减、日范围、相对时间,以及七个历史日期功能的具名函数。
@@ -7,7 +7,9 @@
7
7
  - Framework boundary: Vue remains external to the package-manager build and is a required peer in `^3.3.0`.
8
8
  - uni-app boundary: the first Storage operation, or an earlier `configureStorage({ prefix })` call, detects global `uni` and uses its synchronous Storage API.
9
9
  - Browser storage: applications import `Local` and `Session` directly; `configureStorage()` is needed only to override defaults before the first operation.
10
- - Stateful browser defaults: Storage and Identity configuration are page-global by design. Conflicting reconfiguration throws.
10
+ - Storage operation overrides: `set/get({ crypto })` select JSON or Base64 for one operation without mutating global configuration. The v3 envelope does not identify its codec, so callers must use matching options for the same entry.
11
+ - Storage read typing: `get<Value = string>()` defaults to `string | undefined` when no generic is supplied; codecs still restore the original runtime JSON value.
12
+ - Stateful browser defaults: Storage, Identity, and default Logger configuration are page-global by design. Storage and Identity reject conflicting reconfiguration; `configureLogger` replaces the default Logger configuration while preserving the exported facade reference.
11
13
  - Randomness: every random generation entry prefers Web Crypto and falls back to `Math.random()` when unavailable.
12
14
  - Publishing: the repository root is the only package, `dist/` is the only build output, and `package.json#exports` is the complete public path whitelist.
13
15
 
@@ -17,7 +19,8 @@ Importing a module does not itself read `window`, browser Storage, or `uni`, so
17
19
 
18
20
  - Stateless Array, Date, String, Number, Object, Base64, Color, DOM, Env, Async, and Crypto capabilities use named exports.
19
21
  - The public API uses named functions instead of mutable aggregate utility objects.
20
- - Stateful browser capabilities use cohesive package-owned objects: `Local`, `Session`, `installationIdentity`, and logger instances. Logger scope is supplied to each severity method rather than stored in child instances.
22
+ - Base64 and Crypto text decoders return primitive strings typed as `DecodedText`. They can be used directly as strings; JSON parsing occurs only through an explicit `.parseJson<T = any>()` call. The first text decode lazily installs a non-enumerable `String.prototype.parseJson` and rejects a foreign same-name property instead of overwriting it. Storage codecs continue to parse JSON automatically.
23
+ - Stateful browser capabilities use cohesive package-owned objects: `Local`, `Session`, `installationIdentity`, and Logger instances. Logger exposes the matching `debug`, `log`, `warn`, and `error` levels, defaults to the `debug` minimum, and receives scope on each method rather than storing it in child instances. `createLogger` returns isolated instances, while `configureLogger` only changes the default `logger` facade.
21
24
  - Internal adapters and client factories are implementation details and are not public export paths.
22
25
  - Removing a named function, changing Storage/ciphertext formats, raising the browser syntax target, or changing the Vue peer range requires an explicit major-version Breaking Change.
23
26
 
@@ -28,10 +31,12 @@ Importing a module does not itself read `window`, browser Storage, or `uni`, so
28
31
  - Vue 边界:Vue 不会打进包管理器使用的构建产物,是 `^3.3.0` 的必需 Peer。
29
32
  - uni-app:首次 Storage 操作或更早的 `configureStorage({ prefix })` 调用会检测全局 `uni`,并使用其同步 Storage API。
30
33
  - Storage:直接从包导入 `Local` 和 `Session` 即可;只有覆盖默认值时才需在首次操作前调用 `configureStorage()`。
31
- - 状态:Storage Identity 配置按浏览器页面全局共享;冲突配置明确抛错。
34
+ - Storage 单次覆盖:`set/get({ crypto })` 只为当前操作选择 JSON 或 Base64,不修改全局配置。v3 包络不记录 Codec,调用方必须对同一条目使用匹配的读写选项。
35
+ - Storage 读取类型:`get<Value = string>()` 未传泛型时默认推断为 `string | undefined`,Codec 在运行时仍恢复原始 JSON 值。
36
+ - 状态:Storage、Identity 与默认 Logger 配置按浏览器页面全局共享。Storage 和 Identity 的冲突配置明确抛错;`configureLogger` 替换默认 Logger 的配置,同时保持导出的门面引用稳定。
32
37
  - 随机数:所有随机生成入口都优先使用 Web Crypto,缺失时回退到 `Math.random()`。
33
38
  - 发布:根目录是唯一 npm 包,`dist/` 是唯一构建输出,`exports` 是完整公共路径白名单。
34
39
 
35
40
  模块导入本身不读取 `window`、浏览器 Storage 或 `uni`,不具备对应平台能力时只在调用相关 API 时明确失败。
36
41
 
37
- 无状态能力统一使用具名导出。有状态浏览器能力使用 `Local`、`Session`、`installationIdentity` 和 Logger 实例;Logger 作用域随每次级别方法调用传入,不创建 Child Logger
42
+ 无状态能力统一使用具名导出。Base64 与 Crypto 文本解码入口返回原始字符串类型 `DecodedText`,可以直接作为字符串使用;JSON 只在显式调用 `.parseJson<T = any>()` 时解析。首次文本解码会按需安装不可枚举的 `String.prototype.parseJson`,若同名属性已被其他实现占用则拒绝覆盖并抛错。Storage Codec 继续自动解析 JSON。有状态浏览器能力使用 `Local`、`Session`、`installationIdentity` 和 Logger 实例;Logger 提供与 Sink 同名的 `debug`、`log`、`warn`、`error` 级别,默认最低级别为 `debug`,作用域随每次调用传入,不创建 Child Logger。`createLogger` 返回隔离实例,`configureLogger` 只修改默认 `logger` 门面。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fast-china/utils",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Typed utilities for modern browsers, WebViews, Vue 3, and uni-app applications.",
5
5
  "type": "module",
6
6
  "keywords": [