没有服务端,没有 socket,没有网络。这个引擎可以是一个你直接调用的 struct,可以是一个 481 KB 的 WebAssembly 模块,也可以是一颗没有操作系统的芯片上的 no_std 库——而且这三种情况下,它是同一个引擎、同一批命令。
每一个必须离线可用的应用,最后都会自己写一层存储。桌面应用会得到 SQLite,外加一套没人想要的 schema。web 应用会得到 localStorage,然后撞上 5 MB 的上限,再发现它是同步的、而且只能存字符串,于是又得到 IndexedDB 和包在它外面的一层抽象。设备会得到一个手写在 flash 里的环形缓冲区。
这三件事是同一个问题,也可以是同一个解。kevy 嵌进去不带任何进程边界,发到浏览器时带着真的 TTL 和真的发布订阅,也能在一颗 Cortex-M 上用一块固定 arena、完全不用分配器地启动。最后这一条,CI 每次 push 都会验一遍。
存储是一个你直接调用的 struct——没有 socket,没有序列化,没有第二个进程。数据是持久的,打开时会重放自己的日志。
# Cargo.toml
kevy-embedded = "4.0"let db = Db::open("data/")?;
db.set(b"session:7f3a", b"{\"user\":\"ada\"}", Some(Duration::from_secs(3600)))?;
assert_eq!(db.get(b"session:7f3a")?.is_some(), true);db.listen("127.0.0.1:6379")?;其他进程通过 RESP 访问同一份存储,上面的代码一行都不用改。
成本与限制 嵌入式的存储不是共享的。数据目录归一个进程所有,如果第二个进程也要这份数据,那正是上面那个 listener——或者完整的服务端——存在的意义。进程内同样可以给 store 一个 RAM 预算(with_tier_budget):冷值下沉到磁盘、在你的进程里换回——照实说一句:进程内的一次冷读会在读的时长内持有 store 的锁,所以可下沉的最大值默认封在 256 KiB。细节在分层存储指南里。
gzip 之后 481 KB。落在浏览器自己的文件系统上,刷新之后还在,发布订阅还能跨标签页。
import { open } from "@goliapkg/kevy";
const db = await open({ persist: { name: "app" } });db.set("cart:u881", JSON.stringify(items), { ttlMs: 86_400_000 });
db.get("cart:u881"); // still there after a reload
db.pttl("cart:u881"); // the engine expires it, not your codedb.subscribe("sync", (payload) => merge(payload));成本与限制 做一次小的同步读,localStorage 更快——它就是页面自己地址空间里的一个 map,任何建在 OPFS 上的东西都赢不了它这一点。kevy 赢在那些本来就让 localStorage 不该被用的地方:真的 TTL、没有 5 MB 上限、value 是字节而不是字符串、写入不挡主线程。
no_std,没有分配器,没有操作系统:存储住在一块由你自己定大小的固定 arena 里,CI 每次 push 都会把它启动一遍。
# Cargo.toml
kevy-store = { version = "4.0", default-features = false }let mut arena = [0u8; 64 * 1024];
let mut store = Store::new_in(&mut arena);
store.set(b"temp", b"21.4")?;成本与限制 arena 是固定的。运行时不能再涨——这就是“没有分配器”的含义,它的大小是你的设计决定,不是引擎的。功能分级和每一级要付多少字节,都在 IoT 指南里。