summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock14
-rw-r--r--Cargo.toml1
-rw-r--r--cli/Cargo.toml1
-rw-r--r--cli/bench/cache_api.js56
-rw-r--r--cli/build.rs5
-rw-r--r--cli/dts/lib.deno.window.d.ts4
-rw-r--r--cli/dts/lib.deno.worker.d.ts2
-rw-r--r--cli/lsp/tsc.rs2
-rw-r--r--cli/main.rs1
-rw-r--r--cli/standalone.rs1
-rw-r--r--cli/tests/unit/cache_api_test.ts96
-rw-r--r--cli/tests/unit/fetch_test.ts1
-rw-r--r--cli/tests/unit/test_util.ts1
-rw-r--r--cli/tsc.rs1
-rw-r--r--cli/worker.rs20
-rw-r--r--core/resources.rs4
-rw-r--r--ext/cache/01_cache.js287
-rw-r--r--ext/cache/Cargo.toml22
-rw-r--r--ext/cache/README.md24
-rw-r--r--ext/cache/lib.deno_cache.d.ts72
-rw-r--r--ext/cache/lib.rs214
-rw-r--r--ext/cache/sqlite.rs503
-rw-r--r--ext/fetch/20_headers.js5
-rw-r--r--runtime/Cargo.toml2
-rw-r--r--runtime/build.rs2
-rw-r--r--runtime/examples/hello_runtime.rs1
-rw-r--r--runtime/js/99_main.js8
-rw-r--r--runtime/lib.rs1
-rw-r--r--runtime/web_worker.rs8
-rw-r--r--runtime/worker.rs9
m---------test_util/wpt0
-rw-r--r--tools/wpt/expectation.json268
32 files changed, 1631 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b6bbbf31a..e4764a854 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -813,6 +813,7 @@ dependencies = [
"deno_ast",
"deno_bench_util",
"deno_broadcast_channel",
+ "deno_cache",
"deno_console",
"deno_core",
"deno_crypto",
@@ -941,6 +942,18 @@ dependencies = [
]
[[package]]
+name = "deno_cache"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "deno_core",
+ "rusqlite",
+ "serde",
+ "sha2",
+ "tokio",
+]
+
+[[package]]
name = "deno_console"
version = "0.70.0"
dependencies = [
@@ -1192,6 +1205,7 @@ version = "0.78.0"
dependencies = [
"atty",
"deno_broadcast_channel",
+ "deno_cache",
"deno_console",
"deno_core",
"deno_crypto",
diff --git a/Cargo.toml b/Cargo.toml
index 91b9a0d65..f6aaf6cdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ members = [
"test_ffi",
"test_util",
"ext/broadcast_channel",
+ "ext/cache",
"ext/console",
"ext/crypto",
"ext/fetch",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 22929fd21..15e236a2b 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -27,6 +27,7 @@ path = "./bench/lsp_bench_standalone.rs"
[build-dependencies]
deno_broadcast_channel = { version = "0.64.0", path = "../ext/broadcast_channel" }
+deno_cache = { version = "0.1.0", path = "../ext/cache" }
deno_console = { version = "0.70.0", path = "../ext/console" }
deno_core = { version = "0.152.0", path = "../core" }
deno_crypto = { version = "0.84.0", path = "../ext/crypto" }
diff --git a/cli/bench/cache_api.js b/cli/bench/cache_api.js
new file mode 100644
index 000000000..3e059e80a
--- /dev/null
+++ b/cli/bench/cache_api.js
@@ -0,0 +1,56 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+const cacheName = "cache-v1";
+const cache = await caches.open(cacheName);
+const req = "https://deno.com";
+
+Deno.bench(
+ `cache_storage_open`,
+ async () => {
+ await caches.open("cache-v2");
+ },
+);
+
+Deno.bench(
+ `cache_storage_has`,
+ async () => {
+ await caches.has("cache-v2");
+ },
+);
+
+Deno.bench(
+ `cache_storage_delete`,
+ async () => {
+ await caches.delete("cache-v2");
+ },
+);
+
+// 100 bytes.
+const loremIpsum =
+ `Lorem ipsum dolor sit amet, consectetur adipiscing…es ligula in libero. Sed dignissim lacinia nunc. `;
+let body;
+for (let index = 1; index <= 110; index++) {
+ body += loremIpsum;
+}
+
+Deno.bench(
+ `cache_put_body_${Math.floor(body.length / 1024)}_KiB`,
+ async () => {
+ await cache.put(req, new Response(body));
+ },
+);
+
+Deno.bench("cache_put_no_body", async () => {
+ await cache.put(
+ "https://deno.land/redirect",
+ Response.redirect("https://deno.com"),
+ );
+});
+
+Deno.bench("cache_match", async () => {
+ await cache.match(req);
+});
+
+Deno.bench("cache_delete", async () => {
+ await cache.delete(req);
+});
diff --git a/cli/build.rs b/cli/build.rs
index 1a4eaa425..df4cd5917 100644
--- a/cli/build.rs
+++ b/cli/build.rs
@@ -81,6 +81,7 @@ fn create_compiler_snapshot(
) {
// libs that are being provided by op crates.
let mut op_crate_libs = HashMap::new();
+ op_crate_libs.insert("deno.cache", deno_cache::get_declaration());
op_crate_libs.insert("deno.console", deno_console::get_declaration());
op_crate_libs.insert("deno.url", deno_url::get_declaration());
op_crate_libs.insert("deno.web", deno_web::get_declaration());
@@ -373,6 +374,10 @@ fn main() {
deno_webstorage::get_declaration().display()
);
println!(
+ "cargo:rustc-env=DENO_CACHE_LIB_PATH={}",
+ deno_cache::get_declaration().display()
+ );
+ println!(
"cargo:rustc-env=DENO_CRYPTO_LIB_PATH={}",
deno_crypto::get_declaration().display()
);
diff --git a/cli/dts/lib.deno.window.d.ts b/cli/dts/lib.deno.window.d.ts
index 9877ffc75..9515f5b23 100644
--- a/cli/dts/lib.deno.window.d.ts
+++ b/cli/dts/lib.deno.window.d.ts
@@ -6,6 +6,7 @@
/// <reference lib="deno.webgpu" />
/// <reference lib="deno.webstorage" />
/// <reference lib="esnext" />
+/// <reference lib="deno.cache" />
/** @category Web APIs */
interface WindowEventMap {
@@ -36,6 +37,7 @@ declare class Window extends EventTarget {
location: Location;
localStorage: Storage;
sessionStorage: Storage;
+ caches: CacheStorage;
addEventListener<K extends keyof WindowEventMap>(
type: K,
@@ -83,6 +85,8 @@ declare var onunhandledrejection:
declare var localStorage: Storage;
/** @category Web Storage API */
declare var sessionStorage: Storage;
+/** @category Cache API */
+declare var caches: CacheStorage;
/** @category Web APIs */
declare class Navigator {
diff --git a/cli/dts/lib.deno.worker.d.ts b/cli/dts/lib.deno.worker.d.ts
index 63826b5ef..10a771807 100644
--- a/cli/dts/lib.deno.worker.d.ts
+++ b/cli/dts/lib.deno.worker.d.ts
@@ -5,6 +5,7 @@
/// <reference lib="deno.shared_globals" />
/// <reference lib="deno.webgpu" />
/// <reference lib="esnext" />
+/// <reference lib="deno.cache" />
/** @category Web Workers */
interface WorkerGlobalScopeEventMap {
@@ -51,6 +52,7 @@ declare class WorkerGlobalScope extends EventTarget {
): void;
Deno: typeof Deno;
+ caches: CacheStorage;
}
/** @category Web APIs */
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index 3560ca547..e198c4fb8 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -3775,7 +3775,7 @@ mod tests {
// You might have found this assertion starts failing after upgrading TypeScript.
// Just update the new number of assets (declaration files) for this number.
- assert_eq!(assets.len(), 70);
+ assert_eq!(assets.len(), 71);
// get some notification when the size of the assets grows
let mut total_size = 0;
diff --git a/cli/main.rs b/cli/main.rs
index 70c8c78d0..2ad5c83c0 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -217,6 +217,7 @@ pub fn get_types(unstable: bool) -> String {
tsc::DENO_BROADCAST_CHANNEL_LIB,
tsc::DENO_NET_LIB,
tsc::SHARED_GLOBALS_LIB,
+ tsc::DENO_CACHE_LIB,
tsc::WINDOW_LIB,
];
diff --git a/cli/standalone.rs b/cli/standalone.rs
index 65a51fde5..0c454ce77 100644
--- a/cli/standalone.rs
+++ b/cli/standalone.rs
@@ -300,6 +300,7 @@ pub async fn run(
module_loader,
npm_resolver: None, // not currently supported
get_error_class_fn: Some(&get_error_class_name),
+ cache_storage_dir: None,
origin_storage_dir: None,
blob_store,
broadcast_channel,
diff --git a/cli/tests/unit/cache_api_test.ts b/cli/tests/unit/cache_api_test.ts
new file mode 100644
index 000000000..4d7c6511b
--- /dev/null
+++ b/cli/tests/unit/cache_api_test.ts
@@ -0,0 +1,96 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertFalse,
+ assertRejects,
+} from "./test_util.ts";
+
+Deno.test(async function cacheStorage() {
+ const cacheName = "cache-v1";
+ const _cache = await caches.open(cacheName);
+ assert(await caches.has(cacheName));
+ assert(await caches.delete(cacheName));
+ assertFalse(await caches.has(cacheName));
+});
+
+Deno.test(async function cacheApi() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+ // Test cache.put() with url string as key.
+ {
+ const req = "https://deno.com";
+ await cache.put(req, new Response("deno.com - key is string"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is string");
+ assert(await cache.delete(req));
+ }
+ // Test cache.put() with url instance as key.
+ {
+ const req = new URL("https://deno.com");
+ await cache.put(req, new Response("deno.com - key is URL"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is URL");
+ assert(await cache.delete(req));
+ }
+ // Test cache.put() with request instance as key.
+ {
+ const req = new Request("https://deno.com");
+ await cache.put(req, new Response("deno.com - key is Request"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is Request");
+ assert(await cache.delete(req));
+ }
+
+ // Test cache.put() throws with response Vary header set to *.
+ {
+ const req = new Request("https://deno.com");
+ assertRejects(
+ async () => {
+ await cache.put(
+ req,
+ new Response("deno.com - key is Request", {
+ headers: { Vary: "*" },
+ }),
+ );
+ },
+ TypeError,
+ "Vary header must not contain '*'",
+ );
+ }
+
+ // Test cache.match() with same url but different values for Vary header.
+ {
+ await cache.put(
+ new Request("https://example.com/", {
+ headers: {
+ "Accept": "application/json",
+ },
+ }),
+ Response.json({ msg: "hello world" }, {
+ headers: {
+ "Content-Type": "application/json",
+ "Vary": "Accept",
+ },
+ }),
+ );
+ const res = await cache.match("https://example.com/");
+ assertEquals(res, undefined);
+ const res2 = await cache.match(
+ new Request("https://example.com/", {
+ headers: { "Accept": "text/html" },
+ }),
+ );
+ assertEquals(res2, undefined);
+
+ const res3 = await cache.match(
+ new Request("https://example.com/", {
+ headers: { "Accept": "application/json" },
+ }),
+ );
+ assertEquals(await res3?.json(), { msg: "hello world" });
+ }
+
+ assert(await caches.delete(cacheName));
+ assertFalse(await caches.has(cacheName));
+});
diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts
index 7a531392d..36c1926f2 100644
--- a/cli/tests/unit/fetch_test.ts
+++ b/cli/tests/unit/fetch_test.ts
@@ -1782,7 +1782,6 @@ Deno.test(
const blob = new Blob(["ok"], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const res = await fetch(url);
- console.log(res);
assert(res.url.startsWith("blob:http://js-unit-tests/"));
assertEquals(res.status, 200);
assertEquals(res.headers.get("content-length"), "2");
diff --git a/cli/tests/unit/test_util.ts b/cli/tests/unit/test_util.ts
index 4ad4b2575..19cad092d 100644
--- a/cli/tests/unit/test_util.ts
+++ b/cli/tests/unit/test_util.ts
@@ -6,6 +6,7 @@ import { resolve } from "../../../test_util/std/path/mod.ts";
export {
assert,
assertEquals,
+ assertFalse,
assertMatch,
assertNotEquals,
assertRejects,
diff --git a/cli/tsc.rs b/cli/tsc.rs
index 1951c5a10..e9800d9d2 100644
--- a/cli/tsc.rs
+++ b/cli/tsc.rs
@@ -45,6 +45,7 @@ pub static DENO_WEBSOCKET_LIB: &str =
include_str!(env!("DENO_WEBSOCKET_LIB_PATH"));
pub static DENO_WEBSTORAGE_LIB: &str =
include_str!(env!("DENO_WEBSTORAGE_LIB_PATH"));
+pub static DENO_CACHE_LIB: &str = include_str!(env!("DENO_CACHE_LIB_PATH"));
pub static DENO_CRYPTO_LIB: &str = include_str!(env!("DENO_CRYPTO_LIB_PATH"));
pub static DENO_BROADCAST_CHANNEL_LIB: &str =
include_str!(env!("DENO_BROADCAST_CHANNEL_LIB_PATH"));
diff --git a/cli/worker.rs b/cli/worker.rs
index cc497630e..0454069fa 100644
--- a/cli/worker.rs
+++ b/cli/worker.rs
@@ -379,13 +379,20 @@ pub async fn create_main_worker(
create_web_worker_pre_execute_module_callback(ps.clone());
let maybe_storage_key = ps.options.resolve_storage_key(&main_module);
- let origin_storage_dir = maybe_storage_key.map(|key| {
+ let origin_storage_dir = maybe_storage_key.as_ref().map(|key| {
ps.dir
.root
// TODO(@crowlKats): change to origin_data for 2.0
.join("location_data")
.join(checksum::gen(&[key.as_bytes()]))
});
+ let cache_storage_dir = maybe_storage_key.map(|key| {
+ // TODO(@satyarohith): storage quota management
+ // Note: we currently use temp_dir() to avoid managing storage size.
+ std::env::temp_dir()
+ .join("deno_cache")
+ .join(checksum::gen(&[key.as_bytes()]))
+ });
let mut extensions = ops::cli_exts(ps.clone());
extensions.append(&mut custom_extensions);
@@ -427,6 +434,7 @@ pub async fn create_main_worker(
module_loader,
npm_resolver: Some(Rc::new(ps.npm_resolver.clone())),
get_error_class_fn: Some(&errors::get_error_class_name),
+ cache_storage_dir,
origin_storage_dir,
blob_store: ps.blob_store.clone(),
broadcast_channel: ps.broadcast_channel.clone(),
@@ -496,6 +504,15 @@ fn create_web_worker_callback(
let extensions = ops::cli_exts(ps.clone());
+ let maybe_storage_key = ps.options.resolve_storage_key(&args.main_module);
+ let cache_storage_dir = maybe_storage_key.map(|key| {
+ // TODO(@satyarohith): storage quota management
+ // Note: we currently use temp_dir() to avoid managing storage size.
+ std::env::temp_dir()
+ .join("deno_cache")
+ .join(checksum::gen(&[key.as_bytes()]))
+ });
+
let options = WebWorkerOptions {
bootstrap: BootstrapOptions {
args: ps.options.argv().clone(),
@@ -538,6 +555,7 @@ fn create_web_worker_callback(
shared_array_buffer_store: Some(ps.shared_array_buffer_store.clone()),
compiled_wasm_module_store: Some(ps.compiled_wasm_module_store.clone()),
stdio: stdio.clone(),
+ cache_storage_dir,
};
WebWorker::bootstrap_from_options(
diff --git a/core/resources.rs b/core/resources.rs
index 82b079201..eaa1fb3cf 100644
--- a/core/resources.rs
+++ b/core/resources.rs
@@ -134,6 +134,10 @@ impl ResourceTable {
/// Returns a unique resource ID, which acts as a key for this resource.
pub fn add_rc<T: Resource>(&mut self, resource: Rc<T>) -> ResourceId {
let resource = resource as Rc<dyn Resource>;
+ self.add_rc_dyn(resource)
+ }
+
+ pub fn add_rc_dyn(&mut self, resource: Rc<dyn Resource>) -> ResourceId {
let rid = self.next_rid;
let removed_resource = self.index.insert(rid, resource);
assert!(removed_resource.is_none());
diff --git a/ext/cache/01_cache.js b/ext/cache/01_cache.js
new file mode 100644
index 000000000..9c624b5d7
--- /dev/null
+++ b/ext/cache/01_cache.js
@@ -0,0 +1,287 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+"use strict";
+
+((window) => {
+ const core = window.__bootstrap.core;
+ const webidl = window.__bootstrap.webidl;
+ const {
+ Symbol,
+ TypeError,
+ ObjectPrototypeIsPrototypeOf,
+ } = window.__bootstrap.primordials;
+ const {
+ Request,
+ toInnerResponse,
+ toInnerRequest,
+ } = window.__bootstrap.fetch;
+ const { URLPrototype } = window.__bootstrap.url;
+ const RequestPrototype = Request.prototype;
+ const { getHeader } = window.__bootstrap.headers;
+ const { readableStreamForRid } = window.__bootstrap.streams;
+
+ class CacheStorage {
+ constructor() {
+ webidl.illegalConstructor();
+ }
+
+ async open(cacheName) {
+ webidl.assertBranded(this, CacheStoragePrototype);
+ const prefix = "Failed to execute 'open' on 'CacheStorage'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ cacheName = webidl.converters["DOMString"](cacheName, {
+ prefix,
+ context: "Argument 1",
+ });
+ const cacheId = await core.opAsync("op_cache_storage_open", cacheName);
+ return new Cache(cacheId);
+ }
+
+ async has(cacheName) {
+ webidl.assertBranded(this, CacheStoragePrototype);
+ const prefix = "Failed to execute 'has' on 'CacheStorage'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ cacheName = webidl.converters["DOMString"](cacheName, {
+ prefix,
+ context: "Argument 1",
+ });
+ return await core.opAsync("op_cache_storage_has", cacheName);
+ }
+
+ async delete(cacheName) {
+ webidl.assertBranded(this, CacheStoragePrototype);
+ const prefix = "Failed to execute 'delete' on 'CacheStorage'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ cacheName = webidl.converters["DOMString"](cacheName, {
+ prefix,
+ context: "Argument 1",
+ });
+ return await core.opAsync("op_cache_storage_delete", cacheName);
+ }
+ }
+
+ const _id = Symbol("id");
+
+ class Cache {
+ /** @type {number} */
+ [_id];
+
+ constructor(cacheId) {
+ this[_id] = cacheId;
+ }
+
+ /** See https://w3c.github.io/ServiceWorker/#dom-cache-put */
+ async put(request, response) {
+ const prefix = "Failed to execute 'put' on 'Cache'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+ request = webidl.converters["RequestInfo_DOMString"](request, {
+ prefix,
+ context: "Argument 1",
+ });
+ response = webidl.converters["Response"](response, {
+ prefix,
+ context: "Argument 2",
+ });
+ // Step 1.
+ let innerRequest = null;
+ // Step 2.
+ if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
+ innerRequest = toInnerRequest(request);
+ } else {
+ // Step 3.
+ innerRequest = toInnerRequest(new Request(request));
+ }
+ // Step 4.
+ const reqUrl = new URL(innerRequest.url());
+ if (reqUrl.protocol !== "http:" && reqUrl.protocol !== "https:") {
+ throw new TypeError(
+ "Request url protocol must be 'http:' or 'https:'",
+ );
+ }
+ if (innerRequest.method !== "GET") {
+ throw new TypeError("Request method must be GET");
+ }
+ // Step 5.
+ const innerResponse = toInnerResponse(response);
+ // Step 6.
+ if (innerResponse.status === 206) {
+ throw new TypeError("Response status must not be 206");
+ }
+ // Step 7.
+ const varyHeader = getHeader(innerResponse.headerList, "vary");
+ if (varyHeader) {
+ const fieldValues = varyHeader.split(",").map((field) => field.trim());
+ for (const fieldValue of fieldValues) {
+ if (
+ fieldValue === "*"
+ ) {
+ throw new TypeError("Vary header must not contain '*'");
+ }
+ }
+ }
+
+ // Step 8.
+ if (innerResponse.body !== null && innerResponse.body.unusable()) {
+ throw new TypeError("Response body must not already used");
+ }
+
+ // Remove fragment from request URL before put.
+ reqUrl.hash = "";
+
+ // Step 9-11.
+ const rid = await core.opAsync(
+ "op_cache_put",
+ {
+ cacheId: this[_id],
+ requestUrl: reqUrl.toString(),
+ responseHeaders: innerResponse.headerList,
+ requestHeaders: innerRequest.headerList,
+ responseHasBody: innerResponse.body !== null,
+ responseStatus: innerResponse.status,
+ responseStatusText: innerResponse.statusMessage,
+ },
+ );
+ if (innerResponse.body) {
+ const reader = innerResponse.body.stream.getReader();
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ await core.shutdown(rid);
+ core.close(rid);
+ break;
+ } else {
+ await core.write(rid, value);
+ }
+ }
+ }
+ // Step 12-19: TODO(@satyarohith): do the insertion in background.
+ }
+
+ /** See https://w3c.github.io/ServiceWorker/#cache-match */
+ async match(request, options) {
+ const prefix = "Failed to execute 'match' on 'Cache'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ request = webidl.converters["RequestInfo_DOMString"](request, {
+ prefix,
+ context: "Argument 1",
+ });
+ const p = await this.#matchAll(request, options);
+ if (p.length > 0) {
+ return p[0];
+ } else {
+ return undefined;
+ }
+ }
+
+ /** See https://w3c.github.io/ServiceWorker/#cache-delete */
+ async delete(request, _options) {
+ const prefix = "Failed to execute 'delete' on 'Cache'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ request = webidl.converters["RequestInfo_DOMString"](request, {
+ prefix,
+ context: "Argument 1",
+ });
+ // Step 1.
+ let r = null;
+ // Step 2.
+ if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
+ r = request;
+ if (request.method !== "GET") {
+ return false;
+ }
+ } else if (
+ typeof request === "string" ||
+ ObjectPrototypeIsPrototypeOf(URLPrototype, request)
+ ) {
+ r = new Request(request);
+ }
+ return await core.opAsync("op_cache_delete", {
+ cacheId: this[_id],
+ requestUrl: r.url,
+ });
+ }
+
+ /** See https://w3c.github.io/ServiceWorker/#cache-matchall
+ *
+ * Note: the function is private as we don't want to expose
+ * this API to the public yet.
+ *
+ * The function will return an array of responses.
+ */
+ async #matchAll(request, _options) {
+ // Step 1.
+ let r = null;
+ // Step 2.
+ if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
+ r = request;
+ if (request.method !== "GET") {
+ return [];
+ }
+ } else if (
+ typeof request === "string" ||
+ ObjectPrototypeIsPrototypeOf(URLPrototype, request)
+ ) {
+ r = new Request(request);
+ }
+
+ // Step 5.
+ const responses = [];
+ // Step 5.2
+ if (r === null) {
+ // Step 5.3
+ // Note: we have to return all responses in the cache when
+ // the request is null.
+ // We deviate from the spec here and return an empty array
+ // as we don't expose matchAll() API.
+ return responses;
+ } else {
+ // Remove the fragment from the request URL.
+ const url = new URL(r.url);
+ url.hash = "";
+ const innerRequest = toInnerRequest(r);
+ const matchResult = await core.opAsync(
+ "op_cache_match",
+ {
+ cacheId: this[_id],
+ requestUrl: url.toString(),
+ requestHeaders: innerRequest.headerList,
+ },
+ );
+ if (matchResult) {
+ const [meta, responseBodyRid] = matchResult;
+ let body = null;
+ if (responseBodyRid !== null) {
+ body = readableStreamForRid(responseBodyRid);
+ }
+ const response = new Response(
+ body,
+ {
+ headers: meta.responseHeaders,
+ status: meta.responseStatus,
+ statusText: meta.responseStatusText,
+ },
+ );
+ responses.push(response);
+ }
+ }
+ // Step 5.4-5.5: don't apply in this context.
+
+ return responses;
+ }
+ }
+
+ webidl.configurePrototype(CacheStorage);
+ webidl.configurePrototype(Cache);
+ const CacheStoragePrototype = CacheStorage.prototype;
+
+ let cacheStorage;
+ window.__bootstrap.caches = {
+ CacheStorage,
+ Cache,
+ cacheStorage() {
+ if (!cacheStorage) {
+ cacheStorage = webidl.createBranded(CacheStorage);
+ }
+ return cacheStorage;
+ },
+ };
+})(this);
diff --git a/ext/cache/Cargo.toml b/ext/cache/Cargo.toml
new file mode 100644
index 000000000..6f6808fe7
--- /dev/null
+++ b/ext/cache/Cargo.toml
@@ -0,0 +1,22 @@
+# Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+[package]
+name = "deno_cache"
+version = "0.1.0"
+authors = ["the Deno authors"]
+edition = "2021"
+license = "MIT"
+readme = "README.md"
+repository = "https://github.com/denoland/deno"
+description = "Implementation of Cache API for Deno"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+async-trait = "0.1"
+deno_core = { version = "0.152.0", path = "../../core" }
+rusqlite = { version = "0.28.0", features = ["unlock_notify", "bundled"] }
+serde = { version = "1.0.129", features = ["derive"] }
+sha2 = "0.10.2"
+tokio = { version = "1.19", features = ["full"] }
diff --git a/ext/cache/README.md b/ext/cache/README.md
new file mode 100644
index 000000000..7e58f6e4e
--- /dev/null
+++ b/ext/cache/README.md
@@ -0,0 +1,24 @@
+# deno_cache
+
+This crate implements the Cache API for Deno.
+
+The following APIs are implemented:
+
+- [`CacheStorage::open()`][cache_storage_open]
+- [`CacheStorage::has()`][cache_storage_has]
+- [`CacheStorage::delete()`][cache_storage_delete]
+- [`Cache::match()`][cache_match]
+- [`Cache::put()`][cache_put]
+- [`Cache::delete()`][cache_delete]
+
+Cache APIs don't support the [query options][query_options] yet.
+
+Spec: https://w3c.github.io/ServiceWorker/#cache-interface
+
+[query_options]: https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions
+[cache_storage_open]: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open
+[cache_storage_has]: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/has
+[cache_storage_delete]: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/delete
+[cache_match]: https://developer.mozilla.org/en-US/docs/Web/API/Cache/match
+[cache_put]: https://developer.mozilla.org/en-US/docs/Web/API/Cache/put
+[cache_delete]: https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete
diff --git a/ext/cache/lib.deno_cache.d.ts b/ext/cache/lib.deno_cache.d.ts
new file mode 100644
index 000000000..3b03512fc
--- /dev/null
+++ b/ext/cache/lib.deno_cache.d.ts
@@ -0,0 +1,72 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-var
+
+/// <reference no-default-lib="true" />
+/// <reference lib="esnext" />
+
+/** @category Cache API */
+declare var caches: CacheStorage;
+
+/** @category Cache API */
+declare interface CacheStorage {
+ /** Open a cache storage for the provided name. */
+ open(cacheName: string): Promise<Cache>;
+ /** Check if cache already exists for the provided name. */
+ has(cacheName: string): Promise<boolean>;
+ /** Delete cache storage for the provided name. */
+ delete(cacheName: string): Promise<boolean>;
+}
+
+/** @category Cache API */
+declare interface Cache {
+ /**
+ * Put the provided request/response into the cache.
+ *
+ * How is the API different from browsers?
+ * 1. You cannot match cache objects using by relative paths.
+ * 2. You cannot pass options like `ignoreVary`, `ignoreMethod`, `ignoreSearch`.
+ */
+ put(request: RequestInfo | URL, response: Response): Promise<void>;
+ /**
+ * Return cache object matching the provided request.
+ *
+ * How is the API different from browsers?
+ * 1. You cannot match cache objects using by relative paths.
+ * 2. You cannot pass options like `ignoreVary`, `ignoreMethod`, `ignoreSearch`.
+ */
+ match(
+ request: RequestInfo | URL,
+ options?: CacheQueryOptions,
+ ): Promise<Response | undefined>;
+ /**
+ * Delete cache object matching the provided request.
+ *
+ * How is the API different from browsers?
+ * 1. You cannot delete cache objects using by relative paths.
+ * 2. You cannot pass options like `ignoreVary`, `ignoreMethod`, `ignoreSearch`.
+ */
+ delete(
+ request: RequestInfo | URL,
+ options?: CacheQueryOptions,
+ ): Promise<boolean>;
+}
+
+/** @category Cache API */
+declare var Cache: {
+ prototype: Cache;
+ new (name: string): Cache;
+};
+
+/** @category Cache API */
+declare var CacheStorage: {
+ prototype: CacheStorage;
+ new (): CacheStorage;
+};
+
+/** @category Cache API */
+interface CacheQueryOptions {
+ ignoreMethod?: boolean;
+ ignoreSearch?: boolean;
+ ignoreVary?: boolean;
+}
diff --git a/ext/cache/lib.rs b/ext/cache/lib.rs
new file mode 100644
index 000000000..350efbc38
--- /dev/null
+++ b/ext/cache/lib.rs
@@ -0,0 +1,214 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+mod sqlite;
+use deno_core::ByteString;
+pub use sqlite::SqliteBackedCache;
+
+use async_trait::async_trait;
+use deno_core::error::AnyError;
+use deno_core::include_js_files;
+use deno_core::op;
+use deno_core::serde::Deserialize;
+use deno_core::serde::Serialize;
+use deno_core::Extension;
+use deno_core::OpState;
+use deno_core::Resource;
+use deno_core::ResourceId;
+
+use std::cell::RefCell;
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::sync::Arc;
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct CachePutRequest {
+ pub cache_id: i64,
+ pub request_url: String,
+ pub request_headers: Vec<(ByteString, ByteString)>,
+ pub response_headers: Vec<(ByteString, ByteString)>,
+ pub response_has_body: bool,
+ pub response_status: u16,
+ pub response_status_text: String,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct CacheMatchRequest {
+ pub cache_id: i64,
+ pub request_url: String,
+ pub request_headers: Vec<(ByteString, ByteString)>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CacheMatchResponse(CacheMatchResponseMeta, Option<ResourceId>);
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CacheMatchResponseMeta {
+ pub response_status: u16,
+ pub response_status_text: String,
+ pub request_headers: Vec<(ByteString, ByteString)>,
+ pub response_headers: Vec<(ByteString, ByteString)>,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct CacheDeleteRequest {
+ pub cache_id: i64,
+ pub request_url: String,
+}
+
+#[async_trait]
+pub trait Cache: Clone {
+ async fn storage_open(&self, cache_name: String) -> Result<i64, AnyError>;
+ async fn storage_has(&self, cache_name: String) -> Result<bool, AnyError>;
+ async fn storage_delete(&self, cache_name: String) -> Result<bool, AnyError>;
+
+ async fn put(
+ &self,
+ request_response: CachePutRequest,
+ ) -> Result<Option<Rc<dyn Resource>>, AnyError>;
+ async fn r#match(
+ &self,
+ request: CacheMatchRequest,
+ ) -> Result<
+ Option<(CacheMatchResponseMeta, Option<Rc<dyn Resource>>)>,
+ AnyError,
+ >;
+ async fn delete(&self, request: CacheDeleteRequest)
+ -> Result<bool, AnyError>;
+}
+
+#[op]
+pub async fn op_cache_storage_open<CA>(
+ state: Rc<RefCell<OpState>>,
+ cache_name: String,
+) -> Result<i64, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ cache.storage_open(cache_name).await
+}
+
+#[op]
+pub async fn op_cache_storage_has<CA>(
+ state: Rc<RefCell<OpState>>,
+ cache_name: String,
+) -> Result<bool, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ cache.storage_has(cache_name).await
+}
+
+#[op]
+pub async fn op_cache_storage_delete<CA>(
+ state: Rc<RefCell<OpState>>,
+ cache_name: String,
+) -> Result<bool, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ cache.storage_delete(cache_name).await
+}
+
+#[op]
+pub async fn op_cache_put<CA>(
+ state: Rc<RefCell<OpState>>,
+ request_response: CachePutRequest,
+) -> Result<Option<ResourceId>, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ match cache.put(request_response).await? {
+ Some(resource) => {
+ let rid = state.borrow_mut().resource_table.add_rc_dyn(resource);
+ Ok(Some(rid))
+ }
+ None => Ok(None),
+ }
+}
+
+#[op]
+pub async fn op_cache_match<CA>(
+ state: Rc<RefCell<OpState>>,
+ request: CacheMatchRequest,
+) -> Result<Option<CacheMatchResponse>, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ match cache.r#match(request).await? {
+ Some((meta, None)) => Ok(Some(CacheMatchResponse(meta, None))),
+ Some((meta, Some(resource))) => {
+ let rid = state.borrow_mut().resource_table.add_rc_dyn(resource);
+ Ok(Some(CacheMatchResponse(meta, Some(rid))))
+ }
+ None => Ok(None),
+ }
+}
+
+#[op]
+pub async fn op_cache_delete<CA>(
+ state: Rc<RefCell<OpState>>,
+ request: CacheDeleteRequest,
+) -> Result<bool, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let cache = get_cache::<CA>(&state)?;
+ cache.delete(request).await
+}
+
+pub fn get_cache<CA>(state: &Rc<RefCell<OpState>>) -> Result<CA, AnyError>
+where
+ CA: Cache + 'static,
+{
+ let mut state = state.borrow_mut();
+ if let Some(cache) = state.try_borrow::<CA>() {
+ Ok(cache.clone())
+ } else {
+ let create_cache = state.borrow::<CreateCache<CA>>().clone();
+ let cache = create_cache.0();
+ state.put(cache);
+ Ok(state.borrow::<CA>().clone())
+ }
+}
+
+#[derive(Clone)]
+pub struct CreateCache<C: Cache + 'static>(pub Arc<dyn Fn() -> C>);
+
+pub fn init<CA: Cache + 'static>(
+ maybe_create_cache: Option<CreateCache<CA>>,
+) -> Extension {
+ Extension::builder()
+ .js(include_js_files!(
+ prefix "deno:ext/cache",
+ "01_cache.js",
+ ))
+ .ops(vec![
+ op_cache_storage_open::decl::<CA>(),
+ op_cache_storage_has::decl::<CA>(),
+ op_cache_storage_delete::decl::<CA>(),
+ op_cache_put::decl::<CA>(),
+ op_cache_match::decl::<CA>(),
+ op_cache_delete::decl::<CA>(),
+ ])
+ .state(move |state| {
+ if let Some(create_cache) = maybe_create_cache.clone() {
+ state.put(create_cache);
+ }
+ Ok(())
+ })
+ .build()
+}
+
+pub fn get_declaration() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_cache.d.ts")
+}
diff --git a/ext/cache/sqlite.rs b/ext/cache/sqlite.rs
new file mode 100644
index 000000000..1e5591839
--- /dev/null
+++ b/ext/cache/sqlite.rs
@@ -0,0 +1,503 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use async_trait::async_trait;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
+use deno_core::AsyncRefCell;
+use deno_core::AsyncResult;
+use deno_core::ByteString;
+use deno_core::Resource;
+use deno_core::ZeroCopyBuf;
+use rusqlite::params;
+use rusqlite::Connection;
+use rusqlite::OptionalExtension;
+use tokio::io::AsyncReadExt;
+use tokio::io::AsyncWriteExt;
+
+use std::borrow::Cow;
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use crate::Cache;
+use crate::CacheDeleteRequest;
+use crate::CacheMatchRequest;
+use crate::CacheMatchResponseMeta;
+use crate::CachePutRequest;
+
+#[derive(Clone)]
+pub struct SqliteBackedCache {
+ pub connection: Arc<Mutex<Connection>>,
+ pub cache_storage_dir: PathBuf,
+}
+
+impl SqliteBackedCache {
+ pub fn new(cache_storage_dir: PathBuf) -> Self {
+ {
+ std::fs::create_dir_all(&cache_storage_dir)
+ .expect("failed to create cache dir");
+ let path = cache_storage_dir.join("cache_metadata.db");
+ let connection = rusqlite::Connection::open(&path).unwrap_or_else(|_| {
+ panic!("failed to open cache db at {}", path.display())
+ });
+ connection
+ .execute(
+ "CREATE TABLE IF NOT EXISTS cache_storage (
+ id INTEGER PRIMARY KEY,
+ cache_name TEXT NOT NULL UNIQUE
+ )",
+ (),
+ )
+ .expect("failed to create cache_storage table");
+ connection
+ .execute(
+ "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)
+ )",
+ (),
+ )
+ .expect("failed to create request_response_list table");
+ SqliteBackedCache {
+ connection: Arc::new(Mutex::new(connection)),
+ cache_storage_dir,
+ }
+ }
+ }
+}
+
+#[async_trait]
+impl Cache for SqliteBackedCache {
+ /// Open a cache storage. Internally, this creates a row in the
+ /// sqlite db if the cache doesn't exist and returns the internal id
+ /// of the cache.
+ async fn storage_open(&self, cache_name: String) -> Result<i64, AnyError> {
+ let db = self.connection.clone();
+ let cache_storage_dir = self.cache_storage_dir.clone();
+ tokio::task::spawn_blocking(move || {
+ let db = db.lock();
+ db.execute(
+ "INSERT OR IGNORE INTO cache_storage (cache_name) VALUES (?1)",
+ params![cache_name],
+ )?;
+ let cache_id = db.query_row(
+ "SELECT id FROM cache_storage WHERE cache_name = ?1",
+ params![cache_name],
+ |row| {
+ let id: i64 = row.get(0)?;
+ Ok(id)
+ },
+ )?;
+ let responses_dir = get_responses_dir(cache_storage_dir, cache_id);
+ std::fs::create_dir_all(&responses_dir)?;
+ Ok::<i64, AnyError>(cache_id)
+ })
+ .await?
+ }
+
+ /// Check if a cache with the provided name exists.
+ /// Note: this doesn't check the disk, it only checks the sqlite db.
+ async fn storage_has(&self, cache_name: String) -> Result<bool, AnyError> {
+ let db = self.connection.clone();
+ tokio::task::spawn_blocking(move || {
+ let db = db.lock();
+ let cache_exists = db.query_row(
+ "SELECT count(cache_name) FROM cache_storage WHERE cache_name = ?1",
+ params![cache_name],
+ |row| {
+ let count: i64 = row.get(0)?;
+ Ok(count > 0)
+ },
+ )?;
+ Ok::<bool, AnyError>(cache_exists)
+ })
+ .await?
+ }
+
+ /// Delete a cache storage. Internally, this deletes the row in the sqlite db.
+ async fn storage_delete(&self, cache_name: String) -> Result<bool, AnyError> {
+ let db = self.connection.clone();
+ let cache_storage_dir = self.cache_storage_dir.clone();
+ tokio::task::spawn_blocking(move || {
+ let db = db.lock();
+ let maybe_cache_id = db
+ .query_row(
+ "DELETE FROM cache_storage WHERE cache_name = ?1 RETURNING id",
+ params![cache_name],
+ |row| {
+ let id: i64 = row.get(0)?;
+ Ok(id)
+ },
+ )
+ .optional()?;
+ if let Some(cache_id) = maybe_cache_id {
+ let cache_dir = cache_storage_dir.join(cache_id.to_string());
+ if cache_dir.exists() {
+ std::fs::remove_dir_all(cache_dir)?;
+ }
+ }
+ Ok::<bool, AnyError>(maybe_cache_id.is_some())
+ })
+ .await?
+ }
+
+ async fn put(
+ &self,
+ request_response: CachePutRequest,
+ ) -> Result<Option<Rc<dyn Resource>>, AnyError> {
+ let db = self.connection.clone();
+ let cache_storage_dir = self.cache_storage_dir.clone();
+ let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
+ let response_body_key = if request_response.response_has_body {
+ Some(hash(&format!(
+ "{}_{}",
+ &request_response.request_url,
+ now.as_nanos()
+ )))
+ } else {
+ None
+ };
+
+ if let Some(body_key) = response_body_key {
+ let responses_dir =
+ get_responses_dir(cache_storage_dir, request_response.cache_id);
+ let response_path = responses_dir.join(&body_key);
+ let file = tokio::fs::File::create(response_path).await?;
+ Ok(Some(Rc::new(CachePutResource {
+ file: AsyncRefCell::new(file),
+ db,
+ put_request: request_response,
+ response_body_key: body_key,
+ start_time: now.as_secs(),
+ })))
+ } else {
+ insert_cache_asset(db, request_response, None).await?;
+ Ok(None)
+ }
+ }
+
+ async fn r#match(
+ &self,
+ request: CacheMatchRequest,
+ ) -> Result<
+ Option<(CacheMatchResponseMeta, Option<Rc<dyn Resource>>)>,
+ AnyError,
+ > {
+ let db = self.connection.clone();
+ let cache_storage_dir = self.cache_storage_dir.clone();
+ let query_result = tokio::task::spawn_blocking(move || {
+ let db = db.lock();
+ let result = db.query_row(
+ "SELECT response_body_key, response_headers, response_status, response_status_text, request_headers
+ FROM request_response_list
+ WHERE cache_id = ?1 AND request_url = ?2",
+ (request.cache_id, &request.request_url),
+ |row| {
+ let response_body_key: Option<String> = row.get(0)?;
+ let response_headers: Vec<u8> = row.get(1)?;
+ let response_status: u16 = row.get(2)?;
+ let response_status_text: String = row.get(3)?;
+ let request_headers: Vec<u8> = row.get(4)?;
+ let response_headers: Vec<(ByteString, ByteString)> = deserialize_headers(&response_headers);
+ let request_headers: Vec<(ByteString, ByteString)> = deserialize_headers(&request_headers);
+ Ok((CacheMatchResponseMeta {request_headers, response_headers,response_status,response_status_text}, response_body_key))
+ },
+ );
+ result.optional()
+ })
+ .await??;
+
+ match query_result {
+ Some((cache_meta, Some(response_body_key))) => {
+ // From https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
+ // If there's Vary header in the response, ensure all the
+ // headers of the cached request match the query request.
+ if let Some(vary_header) =
+ get_header("vary", &cache_meta.response_headers)
+ {
+ if !vary_header_matches(
+ &vary_header,
+ &request.request_headers,
+ &cache_meta.request_headers,
+ ) {
+ return Ok(None);
+ }
+ }
+ let response_path =
+ get_responses_dir(cache_storage_dir, request.cache_id)
+ .join(response_body_key);
+ let file = tokio::fs::File::open(response_path).await?;
+ return Ok(Some((
+ cache_meta,
+ Some(Rc::new(CacheResponseResource::new(file))),
+ )));
+ }
+ Some((cache_meta, None)) => {
+ return Ok(Some((cache_meta, None)));
+ }
+ None => return Ok(None),
+ }
+ }
+
+ async fn delete(
+ &self,
+ request: CacheDeleteRequest,
+ ) -> Result<bool, AnyError> {
+ let db = self.connection.clone();
+ tokio::task::spawn_blocking(move || {
+ // TODO(@satyarohith): remove the response body from disk if one exists
+ let db = db.lock();
+ let rows_effected = db.execute(
+ "DELETE FROM request_response_list WHERE cache_id = ?1 AND request_url = ?2",
+ (request.cache_id, &request.request_url),
+ )?;
+ Ok::<bool, AnyError>(rows_effected > 0)
+ })
+ .await?
+ }
+}
+
+async fn insert_cache_asset(
+ db: Arc<Mutex<rusqlite::Connection>>,
+ put: CachePutRequest,
+ body_key_start_time: Option<(String, u64)>,
+) -> Result<Option<String>, deno_core::anyhow::Error> {
+ tokio::task::spawn_blocking(move || {
+ let maybe_response_body = {
+ let db = db.lock();
+ let mut response_body_key = None;
+ if let Some((body_key, start_time)) = body_key_start_time {
+ response_body_key = Some(body_key);
+ let last_inserted_at = db.query_row("
+ SELECT last_inserted_at FROM request_response_list
+ WHERE cache_id = ?1 AND request_url = ?2",
+ (put.cache_id, &put.request_url), |row| {
+ let last_inserted_at: i64 = row.get(0)?;
+ Ok(last_inserted_at)
+ }).optional()?;
+ if let Some(last_inserted) = last_inserted_at {
+ // Some other worker has already inserted this resource into the cache.
+ // Note: okay to unwrap() as it is always present when response_body_key is present.
+ if start_time > (last_inserted as u64) {
+ return Ok(None);
+ }
+ }
+ }
+ db.query_row(
+ "INSERT OR REPLACE INTO request_response_list
+ (cache_id, request_url, request_headers, response_headers,
+ response_body_key, response_status, response_status_text, last_inserted_at)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
+ RETURNING response_body_key",
+ (
+ put.cache_id,
+ put.request_url,
+ serialize_headers(&put.request_headers),
+ serialize_headers(&put.response_headers),
+ response_body_key,
+ put.response_status,
+ put.response_status_text,
+ SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
+ ),
+ |row| {
+ let response_body_key: Option<String> = row.get(0)?;
+ Ok(response_body_key)
+ },
+ )?
+ };
+ Ok::<Option<String>, AnyError>(maybe_response_body)
+ }).await?
+}
+
+#[inline]
+fn get_responses_dir(cache_storage_dir: PathBuf, cache_id: i64) -> PathBuf {
+ cache_storage_dir
+ .join(cache_id.to_string())
+ .join("responses")
+}
+
+/// Check if the headers provided in the vary_header match
+/// the query request headers and the cached request headers.
+fn vary_header_matches(
+ vary_header: &ByteString,
+ query_request_headers: &[(ByteString, ByteString)],
+ cached_request_headers: &[(ByteString, ByteString)],
+) -> bool {
+ let vary_header = match std::str::from_utf8(vary_header) {
+ Ok(vary_header) => vary_header,
+ Err(_) => return false,
+ };
+ let headers = get_headers_from_vary_header(vary_header);
+ for header in headers {
+ let query_header = get_header(&header, query_request_headers);
+ let cached_header = get_header(&header, cached_request_headers);
+ if query_header != cached_header {
+ return false;
+ }
+ }
+ true
+}
+
+fn get_headers_from_vary_header(vary_header: &str) -> Vec<String> {
+ vary_header
+ .split(',')
+ .map(|s| s.trim().to_lowercase())
+ .collect()
+}
+
+fn get_header(
+ name: &str,
+ headers: &[(ByteString, ByteString)],
+) -> Option<ByteString> {
+ headers
+ .iter()
+ .find(|(k, _)| {
+ if let Ok(k) = std::str::from_utf8(k) {
+ k.eq_ignore_ascii_case(name)
+ } else {
+ false
+ }
+ })
+ .map(|(_, v)| v.to_owned())
+}
+
+impl deno_core::Resource for SqliteBackedCache {
+ fn name(&self) -> std::borrow::Cow<str> {
+ "SqliteBackedCache".into()
+ }
+}
+
+pub struct CachePutResource {
+ pub db: Arc<Mutex<rusqlite::Connection>>,
+ pub put_request: CachePutRequest,
+ pub response_body_key: String,
+ pub file: AsyncRefCell<tokio::fs::File>,
+ pub start_time: u64,
+}
+
+impl CachePutResource {
+ async fn write(self: Rc<Self>, data: ZeroCopyBuf) -> Result<usize, AnyError> {
+ let resource = deno_core::RcRef::map(&self, |r| &r.file);
+ let mut file = resource.borrow_mut().await;
+ file.write_all(&data).await?;
+ Ok(data.len())
+ }
+
+ async fn shutdown(self: Rc<Self>) -> Result<(), AnyError> {
+ let resource = deno_core::RcRef::map(&self, |r| &r.file);
+ let mut file = resource.borrow_mut().await;
+ file.flush().await?;
+ file.sync_all().await?;
+ insert_cache_asset(
+ self.db.clone(),
+ self.put_request.clone(),
+ Some((self.response_body_key.clone(), self.start_time)),
+ )
+ .await?;
+ Ok(())
+ }
+}
+
+impl Resource for CachePutResource {
+ fn name(&self) -> Cow<str> {
+ "CachePutResource".into()
+ }
+
+ fn write(self: Rc<Self>, buf: ZeroCopyBuf) -> AsyncResult<usize> {
+ Box::pin(self.write(buf))
+ }
+
+ fn shutdown(self: Rc<Self>) -> AsyncResult<()> {
+ Box::pin(self.shutdown())
+ }
+}
+
+pub struct CacheResponseResource {
+ file: AsyncRefCell<tokio::fs::File>,
+}
+
+impl CacheResponseResource {
+ fn new(file: tokio::fs::File) -> Self {
+ Self {
+ file: AsyncRefCell::new(file),
+ }
+ }
+
+ async fn read(
+ self: Rc<Self>,
+ mut buf: ZeroCopyBuf,
+ ) -> Result<(usize, ZeroCopyBuf), AnyError> {
+ let resource = deno_core::RcRef::map(&self, |r| &r.file);
+ let mut file = resource.borrow_mut().await;
+ let nread = file.read(&mut buf).await?;
+ Ok((nread, buf))
+ }
+}
+
+impl Resource for CacheResponseResource {
+ fn name(&self) -> Cow<str> {
+ "CacheResponseResource".into()
+ }
+
+ fn read_return(
+ self: Rc<Self>,
+ buf: ZeroCopyBuf,
+ ) -> AsyncResult<(usize, ZeroCopyBuf)> {
+ Box::pin(self.read(buf))
+ }
+}
+
+pub fn hash(token: &str) -> String {
+ use sha2::Digest;
+ format!("{:x}", sha2::Sha256::digest(token.as_bytes()))
+}
+
+fn serialize_headers(headers: &[(ByteString, ByteString)]) -> Vec<u8> {
+ let mut serialized_headers = Vec::new();
+ for (name, value) in headers {
+ serialized_headers.extend_from_slice(name);
+ serialized_headers.extend_from_slice(b"\r\n");
+ serialized_headers.extend_from_slice(value);
+ serialized_headers.extend_from_slice(b"\r\n");
+ }
+ serialized_headers
+}
+
+fn deserialize_headers(
+ serialized_headers: &[u8],
+) -> Vec<(ByteString, ByteString)> {
+ let mut headers = Vec::new();
+ let mut piece = None;
+ let mut start = 0;
+ for (i, byte) in serialized_headers.iter().enumerate() {
+ if byte == &b'\r' && serialized_headers.get(i + 1) == Some(&b'\n') {
+ if piece.is_none() {
+ piece = Some(start..i);
+ } else {
+ let name = piece.unwrap();
+ let value = start..i;
+ headers.push((
+ ByteString::from(&serialized_headers[name]),
+ ByteString::from(&serialized_headers[value]),
+ ));
+ piece = None;
+ }
+ start = i + 2;
+ }
+ }
+ assert!(piece.is_none());
+ assert_eq!(start, serialized_headers.len());
+ headers
+}
diff --git a/ext/fetch/20_headers.js b/ext/fetch/20_headers.js
index 11a5d29b4..5243c5029 100644
--- a/ext/fetch/20_headers.js
+++ b/ext/fetch/20_headers.js
@@ -465,11 +465,12 @@
}
window.__bootstrap.headers = {
- Headers,
headersFromHeaderList,
headerListFromHeaders,
- fillHeaders,
getDecodeSplitHeader,
guardFromHeaders,
+ fillHeaders,
+ getHeader,
+ Headers,
};
})(this);
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 0084da3c9..fb49439a4 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -23,6 +23,7 @@ path = "examples/hello_runtime.rs"
[build-dependencies]
deno_broadcast_channel = { version = "0.64.0", path = "../ext/broadcast_channel" }
+deno_cache = { version = "0.1.0", path = "../ext/cache" }
deno_console = { version = "0.70.0", path = "../ext/console" }
deno_core = { version = "0.152.0", path = "../core" }
deno_crypto = { version = "0.84.0", path = "../ext/crypto" }
@@ -48,6 +49,7 @@ winapi = "0.3.9"
[dependencies]
deno_broadcast_channel = { version = "0.64.0", path = "../ext/broadcast_channel" }
+deno_cache = { version = "0.1.0", path = "../ext/cache" }
deno_console = { version = "0.70.0", path = "../ext/console" }
deno_core = { version = "0.152.0", path = "../core" }
deno_crypto = { version = "0.84.0", path = "../ext/crypto" }
diff --git a/runtime/build.rs b/runtime/build.rs
index d45c4a2a8..55a89fb8b 100644
--- a/runtime/build.rs
+++ b/runtime/build.rs
@@ -8,6 +8,7 @@ use std::path::PathBuf;
#[cfg(not(feature = "docsrs"))]
mod not_docs {
use super::*;
+ use deno_cache::SqliteBackedCache;
use deno_core::Extension;
use deno_core::JsRuntime;
use deno_core::RuntimeOptions;
@@ -175,6 +176,7 @@ mod not_docs {
Default::default(),
),
deno_fetch::init::<Permissions>(Default::default()),
+ deno_cache::init::<SqliteBackedCache>(None),
deno_websocket::init::<Permissions>("".to_owned(), None, None),
deno_webstorage::init(None),
deno_crypto::init(None),
diff --git a/runtime/examples/hello_runtime.rs b/runtime/examples/hello_runtime.rs
index de5c2427d..b4a8d8201 100644
--- a/runtime/examples/hello_runtime.rs
+++ b/runtime/examples/hello_runtime.rs
@@ -55,6 +55,7 @@ async fn main() -> Result<(), AnyError> {
module_loader,
npm_resolver: None,
get_error_class_fn: Some(&get_error_class_name),
+ cache_storage_dir: None,
origin_storage_dir: None,
blob_store: BlobStore::default(),
broadcast_channel: InMemoryBroadcastChannel::default(),
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index 755eac939..0a65cadee 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -50,6 +50,7 @@ delete Intl.v8BreakIterator;
const encoding = window.__bootstrap.encoding;
const colors = window.__bootstrap.colors;
const Console = window.__bootstrap.console.Console;
+ const caches = window.__bootstrap.caches;
const inspectArgs = window.__bootstrap.console.inspectArgs;
const quoteString = window.__bootstrap.console.quoteString;
const compression = window.__bootstrap.compression;
@@ -469,6 +470,13 @@ delete Intl.v8BreakIterator;
btoa: util.writable(base64.btoa),
clearInterval: util.writable(timers.clearInterval),
clearTimeout: util.writable(timers.clearTimeout),
+ caches: {
+ enumerable: true,
+ configurable: true,
+ get: caches.cacheStorage,
+ },
+ CacheStorage: util.nonEnumerable(caches.CacheStorage),
+ Cache: util.nonEnumerable(caches.Cache),
console: util.nonEnumerable(
new Console((msg, level) => core.print(msg, level > 1)),
),
diff --git a/runtime/lib.rs b/runtime/lib.rs
index 656662391..99813e3d8 100644
--- a/runtime/lib.rs
+++ b/runtime/lib.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
pub use deno_broadcast_channel;
+pub use deno_cache;
pub use deno_console;
pub use deno_core;
pub use deno_crypto;
diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs
index 306e1da5c..09a631916 100644
--- a/runtime/web_worker.rs
+++ b/runtime/web_worker.rs
@@ -9,6 +9,8 @@ use crate::tokio_util::run_local;
use crate::worker::FormatJsErrorFn;
use crate::BootstrapOptions;
use deno_broadcast_channel::InMemoryBroadcastChannel;
+use deno_cache::CreateCache;
+use deno_cache::SqliteBackedCache;
use deno_core::error::AnyError;
use deno_core::error::JsError;
use deno_core::futures::channel::mpsc;
@@ -337,6 +339,7 @@ pub struct WebWorkerOptions {
pub broadcast_channel: InMemoryBroadcastChannel,
pub shared_array_buffer_store: Option<SharedArrayBufferStore>,
pub compiled_wasm_module_store: Option<CompiledWasmModuleStore>,
+ pub cache_storage_dir: Option<std::path::PathBuf>,
pub stdio: Stdio,
}
@@ -373,6 +376,10 @@ impl WebWorker {
Ok(())
})
.build();
+ let create_cache = options.cache_storage_dir.map(|storage_dir| {
+ let create_cache_fn = move || SqliteBackedCache::new(storage_dir.clone());
+ CreateCache(Arc::new(create_cache_fn))
+ });
let mut extensions: Vec<Extension> = vec![
// Web APIs
@@ -392,6 +399,7 @@ impl WebWorker {
file_fetch_handler: Rc::new(deno_fetch::FsFetchHandler),
..Default::default()
}),
+ deno_cache::init::<SqliteBackedCache>(create_cache),
deno_websocket::init::<Permissions>(
options.bootstrap.user_agent.clone(),
options.root_cert_store.clone(),
diff --git a/runtime/worker.rs b/runtime/worker.rs
index 0723cef84..3ac3654e2 100644
--- a/runtime/worker.rs
+++ b/runtime/worker.rs
@@ -7,6 +7,8 @@ use crate::ops::io::Stdio;
use crate::permissions::Permissions;
use crate::BootstrapOptions;
use deno_broadcast_channel::InMemoryBroadcastChannel;
+use deno_cache::CreateCache;
+use deno_cache::SqliteBackedCache;
use deno_core::error::AnyError;
use deno_core::error::JsError;
use deno_core::futures::Future;
@@ -85,6 +87,7 @@ pub struct WorkerOptions {
pub maybe_inspector_server: Option<Arc<InspectorServer>>,
pub should_break_on_first_statement: bool,
pub get_error_class_fn: Option<GetErrorClassFn>,
+ pub cache_storage_dir: Option<std::path::PathBuf>,
pub origin_storage_dir: Option<std::path::PathBuf>,
pub blob_store: BlobStore,
pub broadcast_channel: InMemoryBroadcastChannel,
@@ -131,6 +134,10 @@ impl MainWorker {
})
.build();
let exit_code = ExitCode(Arc::new(AtomicI32::new(0)));
+ let create_cache = options.cache_storage_dir.map(|storage_dir| {
+ let create_cache_fn = move || SqliteBackedCache::new(storage_dir.clone());
+ CreateCache(Arc::new(create_cache_fn))
+ });
// Internal modules
let mut extensions: Vec<Extension> = vec![
@@ -151,6 +158,7 @@ impl MainWorker {
file_fetch_handler: Rc::new(deno_fetch::FsFetchHandler),
..Default::default()
}),
+ deno_cache::init::<SqliteBackedCache>(create_cache),
deno_websocket::init::<Permissions>(
options.bootstrap.user_agent.clone(),
options.root_cert_store.clone(),
@@ -527,6 +535,7 @@ mod tests {
module_loader: Rc::new(deno_core::FsModuleLoader),
npm_resolver: None,
get_error_class_fn: None,
+ cache_storage_dir: None,
origin_storage_dir: None,
blob_store: BlobStore::default(),
broadcast_channel: InMemoryBroadcastChannel::default(),
diff --git a/test_util/wpt b/test_util/wpt
-Subproject 5d424eb8c75dee2c8c11f4d5db17ee4e31fe1a7
+Subproject 5754d7dddf4b66581facc657320ab533526ab00
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index b5df6cacf..59688934f 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -4642,5 +4642,273 @@
"idlharness.https.any.html": true,
"idlharness.https.any.worker.html": true,
"idlharness-shadowrealm.window.html": false
+ },
+ "service-workers": {
+ "idlharness.https.any.html": [
+ "ServiceWorker interface: existence and properties of interface object",
+ "ServiceWorker interface object length",
+ "ServiceWorker interface object name",
+ "ServiceWorker interface: existence and properties of interface prototype object",
+ "ServiceWorker interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorker interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorker interface: attribute scriptURL",
+ "ServiceWorker interface: attribute state",
+ "ServiceWorker interface: operation postMessage(any, sequence<object>)",
+ "ServiceWorker interface: operation postMessage(any, optional StructuredSerializeOptions)",
+ "ServiceWorker interface: attribute onstatechange",
+ "ServiceWorker must be primary interface of registrationInstance.installing",
+ "Stringification of registrationInstance.installing",
+ "ServiceWorker interface: registrationInstance.installing must inherit property \"scriptURL\" with the proper type",
+ "ServiceWorker interface: registrationInstance.installing must inherit property \"state\" with the proper type",
+ "ServiceWorker interface: registrationInstance.installing must inherit property \"postMessage(any, sequence<object>)\" with the proper type",
+ "ServiceWorker interface: calling postMessage(any, sequence<object>) on registrationInstance.installing with too few arguments must throw TypeError",
+ "ServiceWorker interface: registrationInstance.installing must inherit property \"postMessage(any, optional StructuredSerializeOptions)\" with the proper type",
+ "ServiceWorker interface: calling postMessage(any, optional StructuredSerializeOptions) on registrationInstance.installing with too few arguments must throw TypeError",
+ "ServiceWorker interface: registrationInstance.installing must inherit property \"onstatechange\" with the proper type",
+ "ServiceWorkerRegistration interface: existence and properties of interface object",
+ "ServiceWorkerRegistration interface object length",
+ "ServiceWorkerRegistration interface object name",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorkerRegistration interface: attribute installing",
+ "ServiceWorkerRegistration interface: attribute waiting",
+ "ServiceWorkerRegistration interface: attribute active",
+ "ServiceWorkerRegistration interface: attribute navigationPreload",
+ "ServiceWorkerRegistration interface: attribute scope",
+ "ServiceWorkerRegistration interface: attribute updateViaCache",
+ "ServiceWorkerRegistration interface: operation update()",
+ "ServiceWorkerRegistration interface: operation unregister()",
+ "ServiceWorkerRegistration interface: attribute onupdatefound",
+ "ServiceWorkerRegistration must be primary interface of registrationInstance",
+ "Stringification of registrationInstance",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"installing\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"waiting\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"active\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"navigationPreload\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"scope\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"updateViaCache\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"update()\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"unregister()\" with the proper type",
+ "ServiceWorkerRegistration interface: registrationInstance must inherit property \"onupdatefound\" with the proper type",
+ "ServiceWorkerContainer interface: existence and properties of interface object",
+ "ServiceWorkerContainer interface object length",
+ "ServiceWorkerContainer interface object name",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorkerContainer interface: attribute controller",
+ "ServiceWorkerContainer interface: attribute ready",
+ "ServiceWorkerContainer interface: operation register(USVString, optional RegistrationOptions)",
+ "ServiceWorkerContainer interface: operation getRegistration(optional USVString)",
+ "ServiceWorkerContainer interface: operation getRegistrations()",
+ "ServiceWorkerContainer interface: operation startMessages()",
+ "ServiceWorkerContainer interface: attribute oncontrollerchange",
+ "ServiceWorkerContainer interface: attribute onmessage",
+ "ServiceWorkerContainer interface: attribute onmessageerror",
+ "ServiceWorkerContainer must be primary interface of navigator.serviceWorker",
+ "Stringification of navigator.serviceWorker",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"controller\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"ready\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"register(USVString, optional RegistrationOptions)\" with the proper type",
+ "ServiceWorkerContainer interface: calling register(USVString, optional RegistrationOptions) on navigator.serviceWorker with too few arguments must throw TypeError",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"getRegistration(optional USVString)\" with the proper type",
+ "ServiceWorkerContainer interface: calling getRegistration(optional USVString) on navigator.serviceWorker with too few arguments must throw TypeError",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"getRegistrations()\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"startMessages()\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"oncontrollerchange\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"onmessage\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"onmessageerror\" with the proper type",
+ "NavigationPreloadManager interface: existence and properties of interface object",
+ "NavigationPreloadManager interface object length",
+ "NavigationPreloadManager interface object name",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object's \"constructor\" property",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object's @@unscopables property",
+ "NavigationPreloadManager interface: operation enable()",
+ "NavigationPreloadManager interface: operation disable()",
+ "NavigationPreloadManager interface: operation setHeaderValue(ByteString)",
+ "NavigationPreloadManager interface: operation getState()",
+ "Cache interface: existence and properties of interface object",
+ "Cache interface object length",
+ "Cache interface: operation match(RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation matchAll(optional RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation add(RequestInfo)",
+ "Cache interface: operation addAll(sequence<RequestInfo>)",
+ "Cache interface: operation delete(RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation keys(optional RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: self.cacheInstance must inherit property \"matchAll(optional RequestInfo, optional CacheQueryOptions)\" with the proper type",
+ "Cache interface: calling matchAll(optional RequestInfo, optional CacheQueryOptions) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"add(RequestInfo)\" with the proper type",
+ "Cache interface: calling add(RequestInfo) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"addAll(sequence<RequestInfo>)\" with the proper type",
+ "Cache interface: calling addAll(sequence<RequestInfo>) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"keys(optional RequestInfo, optional CacheQueryOptions)\" with the proper type",
+ "Cache interface: calling keys(optional RequestInfo, optional CacheQueryOptions) on self.cacheInstance with too few arguments must throw TypeError",
+ "CacheStorage interface: operation match(RequestInfo, optional MultiCacheQueryOptions)",
+ "CacheStorage interface: operation keys()",
+ "CacheStorage interface: caches must inherit property \"match(RequestInfo, optional MultiCacheQueryOptions)\" with the proper type",
+ "CacheStorage interface: calling match(RequestInfo, optional MultiCacheQueryOptions) on caches with too few arguments must throw TypeError",
+ "CacheStorage interface: caches must inherit property \"keys()\" with the proper type",
+ "Window interface: attribute caches",
+ "Navigator interface: attribute serviceWorker",
+ "idl_test setup"
+ ],
+ "idlharness.https.any.worker.html": [
+ "ServiceWorker interface: existence and properties of interface object",
+ "ServiceWorker interface object length",
+ "ServiceWorker interface object name",
+ "ServiceWorker interface: existence and properties of interface prototype object",
+ "ServiceWorker interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorker interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorker interface: attribute scriptURL",
+ "ServiceWorker interface: attribute state",
+ "ServiceWorker interface: operation postMessage(any, sequence<object>)",
+ "ServiceWorker interface: operation postMessage(any, optional StructuredSerializeOptions)",
+ "ServiceWorker interface: attribute onstatechange",
+ "ServiceWorkerRegistration interface: existence and properties of interface object",
+ "ServiceWorkerRegistration interface object length",
+ "ServiceWorkerRegistration interface object name",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorkerRegistration interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorkerRegistration interface: attribute installing",
+ "ServiceWorkerRegistration interface: attribute waiting",
+ "ServiceWorkerRegistration interface: attribute active",
+ "ServiceWorkerRegistration interface: attribute navigationPreload",
+ "ServiceWorkerRegistration interface: attribute scope",
+ "ServiceWorkerRegistration interface: attribute updateViaCache",
+ "ServiceWorkerRegistration interface: operation update()",
+ "ServiceWorkerRegistration interface: operation unregister()",
+ "ServiceWorkerRegistration interface: attribute onupdatefound",
+ "ServiceWorkerContainer interface: existence and properties of interface object",
+ "ServiceWorkerContainer interface object length",
+ "ServiceWorkerContainer interface object name",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object's \"constructor\" property",
+ "ServiceWorkerContainer interface: existence and properties of interface prototype object's @@unscopables property",
+ "ServiceWorkerContainer interface: attribute controller",
+ "ServiceWorkerContainer interface: attribute ready",
+ "ServiceWorkerContainer interface: operation register(USVString, optional RegistrationOptions)",
+ "ServiceWorkerContainer interface: operation getRegistration(optional USVString)",
+ "ServiceWorkerContainer interface: operation getRegistrations()",
+ "ServiceWorkerContainer interface: operation startMessages()",
+ "ServiceWorkerContainer interface: attribute oncontrollerchange",
+ "ServiceWorkerContainer interface: attribute onmessage",
+ "ServiceWorkerContainer interface: attribute onmessageerror",
+ "ServiceWorkerContainer must be primary interface of navigator.serviceWorker",
+ "Stringification of navigator.serviceWorker",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"controller\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"ready\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"register(USVString, optional RegistrationOptions)\" with the proper type",
+ "ServiceWorkerContainer interface: calling register(USVString, optional RegistrationOptions) on navigator.serviceWorker with too few arguments must throw TypeError",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"getRegistration(optional USVString)\" with the proper type",
+ "ServiceWorkerContainer interface: calling getRegistration(optional USVString) on navigator.serviceWorker with too few arguments must throw TypeError",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"getRegistrations()\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"startMessages()\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"oncontrollerchange\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"onmessage\" with the proper type",
+ "ServiceWorkerContainer interface: navigator.serviceWorker must inherit property \"onmessageerror\" with the proper type",
+ "NavigationPreloadManager interface: existence and properties of interface object",
+ "NavigationPreloadManager interface object length",
+ "NavigationPreloadManager interface object name",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object's \"constructor\" property",
+ "NavigationPreloadManager interface: existence and properties of interface prototype object's @@unscopables property",
+ "NavigationPreloadManager interface: operation enable()",
+ "NavigationPreloadManager interface: operation disable()",
+ "NavigationPreloadManager interface: operation setHeaderValue(ByteString)",
+ "NavigationPreloadManager interface: operation getState()",
+ "Cache interface: existence and properties of interface object",
+ "Cache interface object length",
+ "Cache interface: operation match(RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation matchAll(optional RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation add(RequestInfo)",
+ "Cache interface: operation addAll(sequence<RequestInfo>)",
+ "Cache interface: operation delete(RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: operation keys(optional RequestInfo, optional CacheQueryOptions)",
+ "Cache interface: self.cacheInstance must inherit property \"matchAll(optional RequestInfo, optional CacheQueryOptions)\" with the proper type",
+ "Cache interface: calling matchAll(optional RequestInfo, optional CacheQueryOptions) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"add(RequestInfo)\" with the proper type",
+ "Cache interface: calling add(RequestInfo) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"addAll(sequence<RequestInfo>)\" with the proper type",
+ "Cache interface: calling addAll(sequence<RequestInfo>) on self.cacheInstance with too few arguments must throw TypeError",
+ "Cache interface: self.cacheInstance must inherit property \"keys(optional RequestInfo, optional CacheQueryOptions)\" with the proper type",
+ "Cache interface: calling keys(optional RequestInfo, optional CacheQueryOptions) on self.cacheInstance with too few arguments must throw TypeError",
+ "CacheStorage interface: operation match(RequestInfo, optional MultiCacheQueryOptions)",
+ "CacheStorage interface: operation keys()",
+ "CacheStorage interface: caches must inherit property \"match(RequestInfo, optional MultiCacheQueryOptions)\" with the proper type",
+ "CacheStorage interface: calling match(RequestInfo, optional MultiCacheQueryOptions) on caches with too few arguments must throw TypeError",
+ "CacheStorage interface: caches must inherit property \"keys()\" with the proper type",
+ "WorkerGlobalScope interface: attribute caches",
+ "WorkerNavigator interface: attribute serviceWorker"
+ ],
+ "cache-storage": {
+ "cache-match.https.any.html": [
+ "Cache.match supports ignoreMethod",
+ "Cache.match supports ignoreVary",
+ "Cache.match with Request and Response objects with different URLs",
+ "Cache.match with a network error Response",
+ "cors-exposed header should be stored correctly.",
+ "MIME type should be set from content-header correctly.",
+ "Cache.match ignores vary headers on opaque response."
+ ],
+ "cache-delete.https.any.html": [
+ "Cache.delete called with a HEAD request",
+ "Cache.delete supports ignoreVary",
+ "Cache.delete with ignoreSearch option (request with search parameters)",
+ "Cache.delete with ignoreSearch option (when it is specified as false)"
+ ],
+ "cache-abort.https.any.html": false,
+ "cache-abort.https.any.worker.html": false,
+ "cache-add.https.any.html": false,
+ "cache-add.https.any.worker.html": false,
+ "cache-delete.https.any.worker.html": [
+ "Cache.delete called with a HEAD request",
+ "Cache.delete supports ignoreVary",
+ "Cache.delete with ignoreSearch option (request with search parameters)",
+ "Cache.delete with ignoreSearch option (when it is specified as false)"
+ ],
+ "cache-keys.https.any.html": false,
+ "cache-keys.https.any.worker.html": false,
+ "cache-match.https.any.worker.html": [
+ "Cache.match supports ignoreMethod",
+ "Cache.match supports ignoreVary",
+ "Cache.match with Request and Response objects with different URLs",
+ "Cache.match with a network error Response",
+ "cors-exposed header should be stored correctly.",
+ "MIME type should be set from content-header correctly.",
+ "Cache.match ignores vary headers on opaque response."
+ ],
+ "cache-matchAll.https.any.html": false,
+ "cache-matchAll.https.any.worker.html": false,
+ "cache-put.https.any.html": [
+ "Cache.put called with Request and Response from fetch()",
+ "Cache.put with opaque-filtered HTTP 206 response",
+ "Cache.put with HTTP 500 response",
+ "Cache.put with a VARY:* opaque response should not reject"
+ ],
+ "cache-put.https.any.worker.html": [
+ "Cache.put called with Request and Response from fetch()",
+ "Cache.put with opaque-filtered HTTP 206 response",
+ "Cache.put with HTTP 500 response",
+ "Cache.put with a VARY:* opaque response should not reject"
+ ],
+ "cache-storage-keys.https.any.html": false,
+ "cache-storage-keys.https.any.worker.html": false,
+ "cache-storage-match.https.any.html": false,
+ "cache-storage-match.https.any.worker.html": false,
+ "cache-storage.https.any.html": [
+ "CacheStorage.delete dooms, but does not delete immediately",
+ "CacheStorage.open with existing cache",
+ "CacheStorage names are DOMStrings not USVStrings"
+ ],
+ "cache-storage.https.any.worker.html": [
+ "CacheStorage.delete dooms, but does not delete immediately",
+ "CacheStorage.open with existing cache",
+ "CacheStorage names are DOMStrings not USVStrings"
+ ],
+ "common.https.window.html": true
+ }
}
} \ No newline at end of file