@flare-engine/储存
为保存游戏和应用程序设置而打入,版本的密钥/值持续. 适配器:原生地上MMKV(sync, sub-milisecond),网络上索引DB,测试用内模. Tiny Zod-ish 计划验证器(s.string(), (中文(简体) ), s.object(),与(.) Infer<S> 类型提取。 仅转发 Migration[] 链被验证 defineMigrationChain ——差距投出于起步,而非出产. 单文件版本商店( E)createDocumentStore)用于设置样式的文档。 冷却出场消毒器san.*用于迁移真实持续数据.
安装
bun add @flare-engine/storage
使用量
import { createStorage, createMemoryAdapter, s, defineMigrationChain } from '@flare-engine/storage';
const SaveSchema = s.object({
version: s.literal(2),
zone: s.number(),
inventory: s.array(s.string()),
});
const migrations = defineMigrationChain([
{
from: 1,
to: 2,
// Lift a v1 { zone, items } save into the v2 shape, preserving the player's data.
migrate: (old: unknown) => {
const v1 = old as { zone?: number; items?: string[] };
return { version: 2, zone: v1.zone ?? 0, inventory: v1.items ?? [] };
},
},
]);
const storage = createStorage({
adapter: createMemoryAdapter(), // in-memory: tests/SSR; swap for MMKV on native (below)
schema: SaveSchema,
version: 2,
migrations,
});
await storage.saveSlot('slot1', { version: 2, zone: 4, inventory: ['key'] });
const data = await storage.loadSlot('slot1');
在本地,在持久的MMKV适配器中互换. react-native-mmkv 是一个 结构 端点 - 您构造 MMKV 并传入;这个包不依赖它(createMmkvAdapter 接受导出 MMKVLike 子集 :
import { MMKV } from 'react-native-mmkv';
import { createStorage, createMmkvAdapter } from '@flare-engine/storage';
const storage = createStorage({
adapter: createMmkvAdapter(new MMKV()),
schema: SaveSchema,
version: 2,
migrations,
});
对设置 doc 使用宽放解析器的单文件存储 :
import { createDocumentStore, createMemoryAdapter, san } from '@flare-engine/storage';
interface Settings { locale: string; volume: number; }
function parseSettings(raw: unknown): Settings {
return san.object({ locale: san.string('en'), volume: san.clamped(1, 0, 1) })(raw);
}
const store = createDocumentStore<Settings>({
adapter: createMemoryAdapter(),
key: 'myapp:settings',
version: 1,
parse: parseSettings,
onFirstRun: (doc) => ({ ...doc, locale: 'en' }),
});
const settings = await store.load();
await store.save({ ...settings, volume: 0.8 });
槽头
插槽菜单每节需要一行(“第12天 ^ Kaszuby ^ 6 h slayed”)而无需付费 loadSlot 在每一个位置上: 运行每个移位和每个插槽的全图解析, 一个腐败的保存扔出并取下菜单。 通过一个小的,JSON-可串行标题作为第三个论点: saveSlot· ; listSlotHeaders() 返回时 { name, version, header } 仅从信封中抽出每个插槽,没有迁移和计划。 在没有标题的情况下保存的插槽, 或在标题存在之前写入, 列表中包含 header: null; 其信封无法读取列表的槽 version: null 财务报告和审定财务报表 header: null一个糟糕的插槽永远不会让其他人失望.
import { createStorage, createMemoryAdapter, s, san } from '@flare-engine/storage';
const SaveSchema = s.object({ day: s.number(), zone: s.string(), inventory: s.array(s.string()) });
// Lenient and never throwing: a header written before `playedSeconds` existed lists it as 0.
const parseHeader = san.object({
day: san.number(0),
zone: san.string('unknown'),
playedSeconds: san.number(0),
});
const storage = createStorage({
adapter: createMemoryAdapter(),
schema: SaveSchema,
version: 1,
parseHeader,
});
await storage.saveSlot(
'slot1',
{ day: 12, zone: 'Kaszuby', inventory: ['key'] },
{ day: 12, zone: 'Kaszuby', playedSeconds: 21_600 },
);
for (const slot of await storage.listSlotHeaders()) {
const label = slot.header
? `Day ${slot.header.day} · ${slot.header.zone} · ${Math.round(slot.header.playedSeconds / 3600)} h played`
: 'Unreadable save';
console.log(slot.name, slot.version ?? '?', label);
}
- 信头没有迁移 .
listSlotHeaders返回已写入的旧页眉,所以修改页眉形状的消费者会读取旧的形状,除非parseHeader修复它.parseHeader宽大一点Sanitizer<H>喜欢createDocumentStore是因为parse; 它也键入H,只运行在listSlotHeaders(单位:千美元)saveSlot按给定键存储页眉),以及使页眉显示为null。 。 。 . - 费用.
listSlotHeaders仍然读取和JSON-parses每个插槽的全字符串;它只跳过迁移和计划. 储蓄是每集 CPU 和 不扔合同,而不是 I/O. - 覆盖删除标题.
saveSlot(name, data)没有页眉,则去掉槽上一个页眉;传递到每个保存上. - 无头
saveSlot写同样{ version, data }信封与以前一样loadSlot忽略了页眉,所以现有的保存不需要迁移.
见 API 规格。 。 。 .
许可证
阿帕奇 - 2.0