首页
/ Deno Cache API 实现详解:deno_cache 如何落地 W3C ServiceWorker Cache 接口

Deno Cache API 实现详解:deno_cache 如何落地 W3C ServiceWorker Cache 接口

2026-09-05 17:10:44作者:史锋燃Gardner

Deno 运行时通过 ext/cache 目录下的 deno_cache crate 实现了 W3C ServiceWorker 规范中的 Cache API(caches 全局对象),为 Web 应用提供基于 SQLite + 本地文件的持久化 HTTP 响应缓存能力。本文以 ext/cache/README.md 为主体,结合 lib.rs01_cache.jssqlite.rs 等源码,梳理已实现的 API 清单、JS/Rust 两层的分工、SQLite 存储结构、Vary 头匹配算法,以及本地 SQLite 与远端 LSC 两种后端的启用方式,帮助你在 Deno 程序中正确使用 caches 并理解其底层实现。

已实现的 API 清单

官方文档 ext/cache/README.md 明确列出了该 crate 当前实现的 API:

接口 说明
CacheStorage::open() 打开(或创建)指定名称的 Cache,返回 Cache 对象
CacheStorage::has() 判断是否存在指定名称的 Cache
CacheStorage::delete() 删除指定名称的 Cache
Cache::match() 按请求匹配缓存项,返回 Responseundefined
Cache::put() 存入 Request/Response 缓存项
Cache::delete() 删除指定请求对应的缓存项

文档同时指出一个重要的适用边界:Cache API 尚不支持 query options(CacheQueryOptions,例如 match()ignoreVaryignoreSearch 等选项暂不可用。

从源码看,当前实现实际上还超出了文档列出的最小集合:lib.rsdeno_cache extension 注册了 8 个 ops,比文档表格多出 op_cache_storage_keys(对应 CacheStorage.keys(),列出所有缓存名称)和 op_cache_keys(对应 Cache.keys(),列出缓存内的请求键);JS 层 01_cache.js01_cache.js 也分别实现了 CacheStorage.keys()Cache.keys()。规范依据是 W3C ServiceWorker 规范中的 cache-interface 章节。

JS 层:WebIDL 校验与请求归一化

JS 侧实现在 01_cache.js,通过 core.loadExtScript 复用 ext:deno_webidlext:deno_fetchext:deno_web 的既有基础设施,定义 CacheStorageCache 两个品牌类(webidl.configureInterface),构造函数均不可直接调用(webidl.illegalConstructor())。每个方法都带规范风格的错误前缀,如 Failed to execute 'open' on 'CacheStorage',参数通过 webidl.converters 做类型校验。

几个关键行为值得注意:

  1. URL 与方法的硬约束Cache.prototype.put 中(01_cache.js):

    • 请求 URL 协议必须是 http:https:,否则抛出 TypeError
    • 请求方法必须是 GET,否则抛出 TypeError
    • 响应状态码不能是 206(Partial Content);
    • 响应头的 Vary 值中不能包含 *
    • 响应 body 若已被消费(unusable)则抛出 TypeError。 另外,put 之前会执行 reqUrl.hash = ""——请求 URL 的 fragment(#...)在存储前被剥离,这与规范中请求匹配忽略 fragment 的语义一致。
  2. 响应体通过 Resource 传递。put 时 JS 层拿到 Response 的 body ReadableStream 的 resource backing(rid),连同请求头、响应头、状态码一起打包成 CachePutRequest 交给 op_cache_put;body 字节流本身不经过 JS 序列化,而是由 Rust 侧直接从 resource 中流式读出。

  3. 跨缓存匹配CacheStorage.match(request, options)01_cache.js)在带 cacheName 选项时只查指定缓存;不带选项时则遍历 op_cache_storage_keys() 返回的所有缓存名依次匹配,第一个命中即返回。

  4. _matchAll 是内部私有方法,注释明确说明"尚未对外暴露 matchAll() API",Cache.prototype.match 内部复用它并取第一个结果。

Rust 层:ops 与双后端架构

Rust 侧 lib.rs 定义了 extension deno_cache(依赖 deno_webidldeno_webdeno_fetch),其 ops 与 JS 方法一一对应:

deno_core::extension!(deno_cache,
  deps = [ deno_webidl, deno_web, deno_fetch ],
  ops = [
    op_cache_storage_open,
    op_cache_storage_has,
    op_cache_storage_delete,
    op_cache_storage_keys,
    op_cache_put,
    op_cache_match,
    op_cache_keys,
    op_cache_delete,
  ],
  lazy_loaded_js = [ "01_cache.js" ],
  options = {
    maybe_create_cache: Option<CreateCache>,
  },
  ...
);

核心抽象是枚举 CacheImpllib.rs),封装两种后端:

pub enum CacheImpl {
  Sqlite(SqliteBackedCache),   // 本地 SQLite 后端
  Lsc(LscBackend),             // 远端 LSC(Live Share Cache)后端
}

CacheImpl 对外暴露统一接口:storage_open / storage_has / storage_delete / storage_keys / put / match / delete / keys,每个方法按实际后端分发。JS 层拿到的是 storage_open 返回的 i64 缓存 id,之后的 match/put/delete/keys 都携带这个 cache_id

后端的注入采用"惰性工厂"模式:extension 只接收一个 CreateCacheArc<dyn Fn() -> Result<CacheImpl, CacheError>>lib.rs),首次调用任意 op 时由 get_cachelib.rs)从 OpState 取出并实例化、缓存;若 State 中既无现成实例也无工厂,则返回 CacheError::ContextUnsupported("CacheStorage is not available in this context")。这就是为什么在非 Deno CLI 主 worker(如未配置存储目录的嵌入式场景)中 caches 会抛错。

错误类型 CacheErrorlib.rs)区分了 ContextUnsupportedEmptyNameNotFoundContentEncodingNotAllowed(LSC 后端禁止在响应头中携带 Content-Encoding)等语义,并映射为 JS 侧的 TypeError#[class(type)])或通用 Error,便于用户代码精确捕获。

SQLite 后端:存储结构与关键流程

本地持久化由 sqlite.rsSqliteBackedCache 实现,采用"SQLite 存元数据 + 目录存响应体"的组合:

初始化与目录布局

SqliteBackedCache::newsqlite.rs)中:

  • 环境变量 DENO_CACHE_DB_MODE 可取 disk(默认)或 memory,未知值会警告并回退到 disk;memory 模式使用 rusqlite::Connection::open_in_memory(),数据随进程消失。
  • disk 模式下先 create_cache_storage_dir 创建存储目录,随后打开 cache_metadata.db 并启用 WAL(PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;)以获得更好的并发读写表现。
  • 安全约束:存储目录不能是符号链接(有专门测试 cache_storage_dir_rejects_symlink 验证);Unix 下目录权限被强制设为 0o700(测试 cache_storage_dir_has_private_permissions),因为缓存中保存的是用户可读取的网络响应,属于敏感数据。

目录结构为:

<cache_storage_dir>/
  cache_metadata.db          # SQLite 元数据库
  <cache_id>/responses/       # 每个缓存一个 responses 目录
      <sha256-key>            # 响应体文件

其中 <cache_id>/responses/get_responses_dirsqlite.rs)拼出;响应体文件名是对 {request_url}_{纳秒时间戳} 做 SHA-256 得到的 hex 串(hash 函数,sqlite.rs),同一 URL 重复 put 不会覆盖旧文件,只由数据库行指向最新 body key。

两张核心表

初始化 SQL(sqlite.rs)建了两张表:

CREATE TABLE IF NOT EXISTS cache_storage (
    id              INTEGER PRIMARY KEY,
    cache_name      TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS request_response_list (
    id                     INTEGER PRIMARY KEY,
    cache_id               INTEGER NOT NULL,
    request_url            TEXT NOT NULL,
    request_headers        BLOB NOT NULL,
    response_headers       BLOB NOT NULL,
    response_status        INTEGER NOT NULL,
    response_status_text   TEXT,
    response_body_key      TEXT,
    last_inserted_at       INTEGER UNSIGNED NOT NULL,
    FOREIGN KEY (cache_id) REFERENCES cache_storage(id) ON DELETE CASCADE,
    UNIQUE (cache_id, request_url)
);

cache_storage 记录缓存名到内部 id 的映射;request_response_list 记录每个缓存项的完整元数据,(cache_id, request_url) 唯一约束保证同一缓存内同一请求 URL 只保留一条记录。头信息以 serialize_headers/deserialize_headerslib.rs)序列化为 name\r\nvalue\r\n... 字节的 BLOB 存储。

put / match / delete / keys 流程

  • putsqlite.rs):若有响应体,先以 64KB 缓冲从 response resource 流式读入 <cache_id>/responses/<body_key> 文件并 sync_all 落盘,再用 INSERT OR REPLACE ... RETURNING response_body_key 插入/替换元数据行;无 body 的响应(如 204)则 response_body_key 为 NULL。
  • matchsqlite.rs):按 (cache_id, request_url) 查库;若响应带 Vary 头,则调用 vary_header_matches 校验请求头是否一致,不一致直接返回未命中;命中后打开响应体文件包装成 CacheResponseResource 交给 JS 重建 Response。若 body 文件丢失(如被手工删除),会 best-efforts 删除该数据库行并返回未命中,避免脏数据。
  • delete / keyskeys 支持传入归一化后的 request_url 在 SQL 层直接过滤(CacheKeysRequest.request_url 注释说明了这一点,lib.rs),避免物化整个缓存;storage_delete 则删库行的同时用 std::fs::remove_dir_all 移除对应的 <cache_id> 目录。

所有数据库操作都通过 spawn_blocking 放到阻塞线程池执行,连接用 Arc<Mutex<Connection>> 保护,主线程不持有数据库锁。

Vary 头匹配算法

Vary 是 Cache API 正确性的核心:它声明响应因哪些请求头而异。lib.rsvary_header_matches 实现规则:

  1. Vary 值按逗号拆分为字段列表(get_headers_from_vary_header 会 trim 并统一小写);
  2. 任意字段为 * 时直接判为不匹配(浏览器规范语义:Vary: * 意味着该缓存项不可被复用,所以 put 时也禁止);
  3. 对每个字段,用 Fetch 的 header-list get 算法(get_headerlib.rs)取两侧请求的合并值——同名头按原始顺序以 , 拼接、名字比较忽略大小写——逐一比较,任一不等即未命中。

文件底部附带的单元测试 test_vary_header_matcheslib.rs)覆盖了:值相同/不同、多值头的顺序敏感性(first, querycached, first 不等)、单侧缺失头、Vary: *、多个 Vary 字段组合等场景,可以作为理解匹配语义的权威参照。

后端如何被启用:从 Worker 注入

deno_cache extension 本身不决定用哪个后端,注入发生在 runtime 装配 Worker 时:

  • runtime/worker.rs(主 worker)与 runtime/web_worker.rs(Web Worker)中的 create_cache_inner 按优先级选择后端:
    1. 若设置了环境变量 DENO_CACHE_LSC_ENDPOINT(格式为 endpoint,token 两段),构造 CacheShard 并使用远端 LscBackendlscache.rs,请求头以 x-lsc-meta-reqhdr- 前缀元数据携带);
    2. 否则若 Worker 配置了 cache_storage_dir,创建本地 SqliteBackedCache
    3. 都没有则返回 Nonecaches 全局对象调用时抛 ContextUnsupported
  • 选定的工厂经 deno_cache::deno_cache::init(create_cache)runtime/web_worker.rs)传入 extension。

cache_storage_dir 的来源在 cli/lib/worker.rs:它取 Deno 的 origin data 目录下的 web_cache 子目录(get_cache_storage_dir),并可用 storage key 做哈希派生子目录,保证不同 origin 的缓存数据相互隔离。

典型使用示例

结合上述实现语义,一段可运行的用法如下(在配置了存储目录的 Deno 环境中):

// 打开(不存在则创建)名为 "my-cache" 的缓存
const cache = await caches.open("my-cache");

// 存入:注意只允许 http/https 的 GET 请求、状态码非 206、Vary 不含 "*"
const req = new Request("https://example.com/data.json");
const res = await fetch(req);
await cache.put(req, res);

// 匹配:fragment 会被忽略;响应带 Vary 时会按请求头校验
const hit = await caches.match("https://example.com/data.json#section");
console.log(hit ? (await hit.json()) : "miss");

// 缓存管理
console.log(await caches.has("my-cache"));   // true
console.log(await caches.keys());           // 列出所有缓存名
const keys = await cache.keys();            // 列出该缓存内所有请求键
await cache.delete("https://example.com/data.json");
await caches.delete("my-cache");            // 删除整个缓存

几个由实现直接决定的注意事项:

  • caches.match 返回的 Response 的 body 是一条真实可读的流(由 CacheResponseResource 提供,lib.rs),只能消费一次;
  • put 会剥离 URL fragment,match/keys/delete 的 URL 归一化逻辑与之保持一致(JS 层统一 url.hash = "");
  • 非 GET 请求传给 cache.delete 时直接返回 false(不会误删同 URL 的 GET 缓存项的语义由 JS 层先行拦截,见 01_cache.js);
  • 由于尚不支持 CacheQueryOptionsignoreVary 等选项无法绕过 Vary 校验。

小结

deno_cache 是 Deno 对 W3C Cache API 的完整工程化落地:JS 层负责 WebIDL 校验、URL/method 约束与 fragment 剥离;Rust 层以 8 个 ops 暴露操作,CacheImpl 枚举同时支持本地 SqliteBackedCache(SQLite 元数据 + 哈希命名响应文件 + WAL + 0o700 目录权限)与远端 LscBackend;Vary 头匹配按 Fetch 规范实现并有独立单元测试背书。文档 ext/cache/README.md 中声明的 6 个 API 是当前承诺的公开面,keys() 系列与私有 _matchAll 则体现了实现的演进空间。阅读 ext/cache/sqlite.rsext/cache/01_cache.js 是最直接的上手路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384