diff options
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 |