Deno Cache API 实现详解:deno_cache 如何落地 W3C ServiceWorker Cache 接口
Deno 运行时通过 ext/cache 目录下的 deno_cache crate 实现了 W3C ServiceWorker 规范中的 Cache API(caches 全局对象),为 Web 应用提供基于 SQLite + 本地文件的持久化 HTTP 响应缓存能力。本文以 ext/cache/README.md 为主体,结合 lib.rs、01_cache.js、sqlite.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() |
按请求匹配缓存项,返回 Response 或 undefined |
Cache::put() |
存入 Request/Response 缓存项 |
Cache::delete() |
删除指定请求对应的缓存项 |
文档同时指出一个重要的适用边界:Cache API 尚不支持 query options(CacheQueryOptions),例如 match() 的 ignoreVary、ignoreSearch 等选项暂不可用。
从源码看,当前实现实际上还超出了文档列出的最小集合:lib.rs 中 deno_cache extension 注册了 8 个 ops,比文档表格多出 op_cache_storage_keys(对应 CacheStorage.keys(),列出所有缓存名称)和 op_cache_keys(对应 Cache.keys(),列出缓存内的请求键);JS 层 01_cache.js 与 01_cache.js 也分别实现了 CacheStorage.keys() 和 Cache.keys()。规范依据是 W3C ServiceWorker 规范中的 cache-interface 章节。
JS 层:WebIDL 校验与请求归一化
JS 侧实现在 01_cache.js,通过 core.loadExtScript 复用 ext:deno_webidl、ext:deno_fetch、ext:deno_web 的既有基础设施,定义 CacheStorage 与 Cache 两个品牌类(webidl.configureInterface),构造函数均不可直接调用(webidl.illegalConstructor())。每个方法都带规范风格的错误前缀,如 Failed to execute 'open' on 'CacheStorage',参数通过 webidl.converters 做类型校验。
几个关键行为值得注意:
-
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 的语义一致。
- 请求 URL 协议必须是
-
响应体通过 Resource 传递。put 时 JS 层拿到
Response的 bodyReadableStream的 resource backing(rid),连同请求头、响应头、状态码一起打包成CachePutRequest交给op_cache_put;body 字节流本身不经过 JS 序列化,而是由 Rust 侧直接从 resource 中流式读出。 -
跨缓存匹配。
CacheStorage.match(request, options)(01_cache.js)在带cacheName选项时只查指定缓存;不带选项时则遍历op_cache_storage_keys()返回的所有缓存名依次匹配,第一个命中即返回。 -
_matchAll是内部私有方法,注释明确说明"尚未对外暴露matchAll()API",Cache.prototype.match内部复用它并取第一个结果。
Rust 层:ops 与双后端架构
Rust 侧 lib.rs 定义了 extension deno_cache(依赖 deno_webidl、deno_web、deno_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>,
},
...
);
核心抽象是枚举 CacheImpl(lib.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 只接收一个 CreateCache(Arc<dyn Fn() -> Result<CacheImpl, CacheError>>,lib.rs),首次调用任意 op 时由 get_cache(lib.rs)从 OpState 取出并实例化、缓存;若 State 中既无现成实例也无工厂,则返回 CacheError::ContextUnsupported("CacheStorage is not available in this context")。这就是为什么在非 Deno CLI 主 worker(如未配置存储目录的嵌入式场景)中 caches 会抛错。
错误类型 CacheError(lib.rs)区分了 ContextUnsupported、EmptyName、NotFound、ContentEncodingNotAllowed(LSC 后端禁止在响应头中携带 Content-Encoding)等语义,并映射为 JS 侧的 TypeError(#[class(type)])或通用 Error,便于用户代码精确捕获。
SQLite 后端:存储结构与关键流程
本地持久化由 sqlite.rs 的 SqliteBackedCache 实现,采用"SQLite 存元数据 + 目录存响应体"的组合:
初始化与目录布局
SqliteBackedCache::new(sqlite.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_dir(sqlite.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_headers(lib.rs)序列化为 name\r\nvalue\r\n... 字节的 BLOB 存储。
put / match / delete / keys 流程
- put(sqlite.rs):若有响应体,先以 64KB 缓冲从 response resource 流式读入
<cache_id>/responses/<body_key>文件并sync_all落盘,再用INSERT OR REPLACE ... RETURNING response_body_key插入/替换元数据行;无 body 的响应(如 204)则response_body_key为 NULL。 - match(sqlite.rs):按
(cache_id, request_url)查库;若响应带Vary头,则调用vary_header_matches校验请求头是否一致,不一致直接返回未命中;命中后打开响应体文件包装成CacheResponseResource交给 JS 重建Response。若 body 文件丢失(如被手工删除),会 best-efforts 删除该数据库行并返回未命中,避免脏数据。 - delete / keys:
keys支持传入归一化后的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.rs 的 vary_header_matches 实现规则:
- 将
Vary值按逗号拆分为字段列表(get_headers_from_vary_header会 trim 并统一小写); - 任意字段为
*时直接判为不匹配(浏览器规范语义:Vary: *意味着该缓存项不可被复用,所以 put 时也禁止); - 对每个字段,用 Fetch 的 header-list get 算法(
get_header,lib.rs)取两侧请求的合并值——同名头按原始顺序以,拼接、名字比较忽略大小写——逐一比较,任一不等即未命中。
文件底部附带的单元测试 test_vary_header_matches(lib.rs)覆盖了:值相同/不同、多值头的顺序敏感性(first, query 与 cached, first 不等)、单侧缺失头、Vary: *、多个 Vary 字段组合等场景,可以作为理解匹配语义的权威参照。
后端如何被启用:从 Worker 注入
deno_cache extension 本身不决定用哪个后端,注入发生在 runtime 装配 Worker 时:
- runtime/worker.rs(主 worker)与 runtime/web_worker.rs(Web Worker)中的
create_cache_inner按优先级选择后端:- 若设置了环境变量
DENO_CACHE_LSC_ENDPOINT(格式为endpoint,token两段),构造CacheShard并使用远端LscBackend(lscache.rs,请求头以x-lsc-meta-reqhdr-前缀元数据携带); - 否则若 Worker 配置了
cache_storage_dir,创建本地SqliteBackedCache; - 都没有则返回
None,caches全局对象调用时抛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); - 由于尚不支持
CacheQueryOptions,ignoreVary等选项无法绕过 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.rs 与 ext/cache/01_cache.js 是最直接的上手路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00