store
store 提供当前设备上、应用作用域、版本化的持久化键值存储。宿主负责本地持久化和并发条件;guest 决定值的格式、索引、加密和领域语义。v0.1 不承诺账户同步或跨设备复制。
基本示例
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const current = await host.store.get('settings/theme');
const settings = current
? JSON.parse(decoder.decode(current.value))
: { theme: 'system' };
await host.store.put(
'settings/theme',
encoder.encode(JSON.stringify({ ...settings, theme: 'dark' })),
current ? { version: current.version } : 'absent',
);
每次写入都显式选择条件,可以防止首次创建和后续更新被并发窗口静默覆盖。
适用场景
- 设置、草稿、进度和小型结构化数据。
- guest 数据库的页、日志或索引块。
- 未来由 guest 通过受控网络能力自行同步的数据页或密文日志;网络能力当前仍是非正式提议。
SQL、查询规划、全文索引、CRDT 和业务 schema 属于 L2。大媒体文件或无限追加日志需要后续专门的对象/流能力,不能挤进 KV。
L1 边界
宿主独占的是跨 guest 生命周期的设备持久化、appId 隔离、配额和崩溃恢复。guest 内存不能替代这项真实存储资源;索引、查询、事务组织、同步和数据模型都可以建立在 KV 之上,因此留在 guest 库。
能力声明
{
"capabilities": [
"host:store.get",
"host:store.put",
"host:store.delete",
"host:store.list",
"host:store.deletePrefix"
]
}
读、写、列举和批量删除逐方法声明。
Reference
- store.get():读取值和当前版本。
- store.put():创建或条件更新。
- store.delete():条件删除单个键。
- store.list():分页列举键与元数据。
- store.deletePrefix():在预算内原子删除一个非空前缀。
数据模型
StoreEntry {
key: string
value: Bytes
version: u64
}
WriteCondition = "any" | "absent" | { version: u64 }
DeleteCondition = "any" | { version: u64 }
StoreCursor = ascii-base64url[1..256]
键是非空、Unicode NFC 规范化的字符串,不得含 U+0000、C0/C1 控制字符或 U+007F;长度按 UTF-8 编码后计算。/ 只是普通字符的约定分隔符,不对应宿主路径。宿主拒绝非 NFC 输入,不能静默改写成另一个键。值是无类型字节;getText、putJSON 等便利方法应由 SDK 提供。
version 是 appId 作用域内单调递增且不复用的 u64。删除后重建同名键也必须取得新版本,防止旧条件发生 ABA 匹配;计数耗尽时写操作以 version-exhausted 永久失败。
分页与并发示例
let cursor: string | undefined;
do {
const page = await host.store.list('documents/', cursor);
for (const item of page.items) renderDocumentRow(item.key, item.version);
cursor = page.cursor;
} while (cursor);
解决乐观并发冲突:
async function increment(key: string) {
for (let attempt = 0; attempt < 3; attempt++) {
const old = await host.store.get(key);
const next = encodeNumber((old ? decodeNumber(old.value) : 0) + 1);
try {
return await host.store.put(key, next, old ? { version: old.version } : 'absent');
} catch (error) {
if (error.code !== 'conflict') throw error;
}
}
throw new Error('too much contention');
}
原生 WASM 示例
uint8_t buffer[4096];
StoreGetResult r = host_store_get("settings/theme", byte_sink(buffer, sizeof(buffer)));
if (r.found) decode_settings(buffer, r.written, r.version);
超过当前 sink 的值返回 required 或可继续读取的明确结果,不能截断后伪装成功。
一致性
put和单键delete对一个键线性化;相同版本条件只能有一个成功。"absent"只在键不存在时创建;"any"明确表示调用方接受覆盖。- 对不存在的键执行
delete(key, "any")返回{ deleted: false };使用{ version }则返回conflict。 list的第一页固定一个键与元数据快照,后续 cursor 必须遍历同一快照;每个应用最多同时持有store.maxListSnapshots个快照。成功返回非末页时开始或重置空闲计时,在不少于store.listSnapshotIdleMs的空闲期内不得过期;末页、空闲到期或 runtime 结束时释放。cursor 不透明,guest 不解析也不持久化。list按 NFC 键的 UTF-8 字节无符号字典序递增返回;cursor 后续调用的 prefix 必须与第一页逐标量相同,否则以invalid-cursor失败。deletePrefix在预算内原子完成;匹配数超过store.maxDeletePrefixItems时必须在删除前失败。- 崩溃恢复后,每次已返回成功的单键写/删或前缀删除必须全部可见;未返回成功的操作可以全部可见或全部不可见,不能部分可见。
安全与预算
- 信任档位:green。
- 数据按 appId 隔离;任何调用都不能覆盖 namespace。
- v0.1 预算键为
store.maxKeyBytes、store.maxValueBytes、store.maxKeys、store.maxTotalBytes、store.maxPageItems、store.maxListSnapshots、store.listSnapshotIdleMs、store.maxDeletePrefixItems、store.writesPerMinute。 deletePrefix必须显示实际删除数量,并受单次和时间窗预算限制。- 错误、日志和遥测不得记录值内容。
错误与测试
特有错误包括 conflict、quota-exceeded、invalid-key、cursor-expired 和 sink-too-small。一致性测试至少覆盖首次写入、并发首次创建、条件更新、并发写、删除竞态、快照分页、拒绝空前缀、原子前缀删除、崩溃恢复、配额和跨应用隔离。