mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 16:19:12 -05:00
feat: implement Web Cache API (#15829)
This commit is contained in:
parent
1156f726a9
commit
b312279e58
32 changed files with 1632 additions and 6 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -813,6 +813,7 @@ dependencies = [
|
|||
"deno_ast",
|
||||
"deno_bench_util",
|
||||
"deno_broadcast_channel",
|
||||
"deno_cache",
|
||||
"deno_console",
|
||||
"deno_core",
|
||||
"deno_crypto",
|
||||
|
@ -940,6 +941,18 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -1192,6 +1205,7 @@ version = "0.78.0"
|
|||
dependencies = [
|
||||
"atty",
|
||||
"deno_broadcast_channel",
|
||||
"deno_cache",
|
||||
"deno_console",
|
||||
"deno_core",
|
||||
"deno_crypto",
|
||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
|||
"test_ffi",
|
||||
"test_util",
|
||||
"ext/broadcast_channel",
|
||||
"ext/cache",
|
||||
"ext/console",
|
||||
"ext/crypto",
|
||||
"ext/fetch",
|
||||
|
|
|
@ -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" }
|
||||
|
|
56
cli/bench/cache_api.js
Normal file
56
cli/bench/cache_api.js
Normal file
|
@ -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);
|
||||
});
|
|
@ -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());
|
||||
|
@ -372,6 +373,10 @@ fn main() {
|
|||
"cargo:rustc-env=DENO_WEBSTORAGE_LIB_PATH={}",
|
||||
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()
|
||||
|
|
4
cli/dts/lib.deno.window.d.ts
vendored
4
cli/dts/lib.deno.window.d.ts
vendored
|
@ -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 {
|
||||
|
|
2
cli/dts/lib.deno.worker.d.ts
vendored
2
cli/dts/lib.deno.worker.d.ts
vendored
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
96
cli/tests/unit/cache_api_test.ts
Normal file
96
cli/tests/unit/cache_api_test.ts
Normal file
|
@ -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));
|
||||
});
|
|
@ -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");
|
||||
|
|
|
@ -6,6 +6,7 @@ import { resolve } from "../../../test_util/std/path/mod.ts";
|
|||
export {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertFalse,
|
||||
assertMatch,
|
||||
assertNotEquals,
|
||||
assertRejects,
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
|
287
ext/cache/01_cache.js
vendored
Normal file
287
ext/cache/01_cache.js
vendored
Normal file
|
@ -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);
|
22
ext/cache/Cargo.toml
vendored
Normal file
22
ext/cache/Cargo.toml
vendored
Normal file
|
@ -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"] }
|
24
ext/cache/README.md
vendored
Normal file
24
ext/cache/README.md
vendored
Normal file
|
@ -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
|
72
ext/cache/lib.deno_cache.d.ts
vendored
Normal file
72
ext/cache/lib.deno_cache.d.ts
vendored
Normal file
|
@ -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;
|
||||
}
|
214
ext/cache/lib.rs
vendored
Normal file
214
ext/cache/lib.rs
vendored
Normal file
|
@ -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")
|
||||
}
|
503
ext/cache/sqlite.rs
vendored
Normal file
503
ext/cache/sqlite.rs
vendored
Normal file
|
@ -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
|
||||
}
|
|
@ -465,11 +465,12 @@
|
|||
}
|
||||
|
||||
window.__bootstrap.headers = {
|
||||
Headers,
|
||||
headersFromHeaderList,
|
||||
headerListFromHeaders,
|
||||
fillHeaders,
|
||||
getDecodeSplitHeader,
|
||||
guardFromHeaders,
|
||||
fillHeaders,
|
||||
getHeader,
|
||||
Headers,
|
||||
};
|
||||
})(this);
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 5d424eb8c75dee2c8c11f4d5db17ee4e31fe1a71
|
||||
Subproject commit 5754d7dddf4b66581facc657320ab533526ab007
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue