v0.1首个正式支持的版本化规范查看版本范围

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

数据模型

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 输入,不能静默改写成另一个键。值是无类型字节;getTextputJSON 等便利方法应由 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.maxKeyBytesstore.maxValueBytesstore.maxKeysstore.maxTotalBytesstore.maxPageItemsstore.maxListSnapshotsstore.listSnapshotIdleMsstore.maxDeletePrefixItemsstore.writesPerMinute
  • deletePrefix 必须显示实际删除数量,并受单次和时间窗预算限制。
  • 错误、日志和遥测不得记录值内容。

错误与测试

特有错误包括 conflictquota-exceededinvalid-keycursor-expiredsink-too-small。一致性测试至少覆盖首次写入、并发首次创建、条件更新、并发写、删除竞态、快照分页、拒绝空前缀、原子前缀删除、崩溃恢复、配额和跨应用隔离。

Host API 版本与分层概览Host API v0.1版本非正式提议提议canvas API呈现canvas.getInfo()方法canvas.draw()方法canvas.present()方法canvas.measureText()方法canvas.frame event事件canvas.resize event事件asset API呈现asset.read()方法asset.loadFont()方法playback API呈现playback.open()方法playback.write()方法playback.stop()方法playback.ready event事件playback.ended event事件input API输入input.focus()方法input.pointer event事件input.key event事件input.wheel event事件input.focusChange event事件ime API输入ime.openSession()方法ime.updateState()方法ime.updateGeometry()方法ime.closeSession()方法ime.textUpdate event事件ime.composition event事件ime.formatUpdate event事件ime.characterBoundsRequest event事件ime.ended event事件capture API输入capture.requestCamera()方法capture.requestMic()方法capture.frame()方法capture.readAudio()方法capture.stop()方法capture.ended event事件store API数据store.get()方法store.put()方法store.delete()方法store.list()方法store.deletePrefix()方法clipboard API数据clipboard.readText()方法clipboard.writeText()方法env API平台集成env.snapshot()方法env.change event事件开始调用 Host API指南构建交互式绘图应用指南构建自绘文本编辑器指南管理媒体采集与播放指南组织本地数据与同步指南