跳转到内容

Storage

Storage 是 localStorage / sessionStorage 的 JSON 读写封装。业务负责 key 命名;实例不创建命名空间,不提供跨 tab 原子更新或数据库事务。

创建与类型约束

ts
import { createStorage } from '@freenotes/web-runtime';

interface AppStorage {
  ready: boolean;
  profile: { name: string } | null;
}

const storage = createStorage<AppStorage>('local');
storage.set('ready', true);
const ready = storage.get('ready'); // boolean | undefined
const profile = storage.get('profile'); // { name: string } | null | undefined

传入 schema 时,getsetupdatehasremove 只接受 schema 中的 key,值完全由 key 推导,不允许另指定值类型。拼错 key、传入错误值或错误 fallback 都会产生类型错误。schema 只提供编译期约束,不验证已存储的数据。

不传 schema 时,允许任意 key,读取默认返回 unknown,可以显式指定值类型或通过 parser 推导类型:

ts
const untyped = createStorage(); // 默认 local
untyped.set('theme', 'dark');
const theme = untyped.get<string>('theme');
const session = createStorage('session');

读取与解析

ts
const ready = storage.get('ready', { fallback: false });
const profile = storage.get('profile', {
  parse(value) {
    if (value === null) return null;
    if (
      typeof value === 'object' &&
      value !== null &&
      'name' in value &&
      typeof value.name === 'string'
    ) {
      return { name: value.name };
    }
    return undefined;
  },
  fallback: null
});

parse 返回解析后的值,undefined 表示无效;null 是合法业务值。读取缺失、过期、版本不匹配、解析返回 undefined 或解析抛异常时,返回 fallback 或 undefined

get()has() 不删除数据。 版本或解析规则由每次调用指定,一个调用者的读取要求不应删除另一个调用者的数据。业务可以显式调用 remove()clearExpired() 清理。

没有 runtime 元数据的历史值仍可读取;原始非 JSON 字符串会按字符串返回。带 version 要求读取裸历史值时,因为无法确认版本,会走 fallback,保留原数据。

写入、过期与版本

ts
storage.set('ready', true, { ttlMs: 60_000, version: 1 });
storage.get('ready', { version: 1, fallback: false });
storage.set('ready', true, { expiresAt: Date.now() + 60_000 });

ttlMs 的单位是毫秒;expiresAt 是 Unix 毫秒时间戳。同时提供时,expiresAt 优先。version 是非负整数。写入 undefined 或无法 JSON 序列化的值返回 false

只有配置了元数据时,存储值才包裹 runtime record;无元数据时直接存储 JSON。set() 是完整替换,不继承旧元数据。expiresAt: nullversion: null 可显式清除对应元数据。

更新

ts
const counters = createStorage<{ count: number }>();
counters.set('count', 1, { ttlMs: 60_000, version: 1 });
counters.update('count', (value) => (value ?? 0) + 1);

counters.update('count', (value) => (value ?? 0) + 1, {
  read: { version: 1, fallback: 0 },
  write: { version: 2 }
});

update 是读取、计算、写入三个步骤,不是跨 tab 的原子操作。成功返回新值,写入失败返回 undefined。业务 updater 抛出的异常会原样传播。

未显式覆盖时,更新保留尚未过期记录的绝对过期时间和版本,不会延长 TTL。write.ttlMs 从本次更新时重新计算过期时间;write.expiresAt: null 清除过期时间;write.version: null 清除版本。过期记录被视为缺失,不继承其元数据。read.versionwrite.version 相互独立。

存储区域与清理

getInfo() 返回 { available, storageType }available 通过一次临时写入探测当前是否可读写,不承诺数据永不丢失。

方法行为
get(key, options?)读取有效值,失败走 fallback,不删除记录。
set(key, value, options?)完整替换值和元数据,返回 boolean。
update(key, updater, options?)更新值,默认继承有效记录的元数据。
has(key)当前是否存在未过期的值,不删除记录。
remove(key)显式删除指定 key。
keys()当前原生存储区域的全部 key,包含过期记录和 schema 外的 key。
clearExpired()删除过期的 runtime record,返回删除数量。
clear()清空整个原生存储区域,返回 boolean。

类型 schema 不是物理命名空间,clear() 不会只清理声明的 key。使用这个方法前,业务必须明确当前 origin 的共享存储范围。

SSR、受限 WebView、存储配额不足时,写入和删除返回 false,读取走 fallback;不会偷偷使用内存替代持久化。浏览器错误和 parser 异常不会进入业务流程。

失败诊断

Storage 是容错存储封装:读取失败仍走 fallback,写入/删除失败仍返回 false。重要数据的调用方可以通过创建时配置的 onError 获取失败原因:

ts
const storage = createStorage<AppStorage>('local', {
  onError(error) {
    reportStorageError({
      kind: error.kind,
      code: error.code,
      operation: error.operation,
      key: error.key,
      cause: error.cause
    });
  }
});

回调参数为 StorageError,包含 kind、ERR_STORAGE_* code、storageType、底层操作 operation、key(适用时)和原始 cause。覆盖环境不可用、非法输入、读写/枚举/删除失败以及 parser 拒绝或抛错。正常缺失、过期和版本不匹配不报告错误。批量操作可能报告多次底层失败。

onError 在创建时捕获并同步调用;回调抛错会被隔离。回调不要再次执行同一存储操作,以免递归。该机制不改变 fallback、删除或写入的行为,也不将 Storage 变成事务数据库。

让基础能力保持简单,让业务开发更加专注。