deno/ext/cache/01_cache.js

297 lines
8.9 KiB
JavaScript

// Copyright 2018-2023 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);
const cache = webidl.createBranded(Cache);
cache[_id] = cacheId;
return cache;
}
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 _matchAll = Symbol("[[matchAll]]");
const _id = Symbol("id");
class Cache {
/** @type {number} */
[_id];
constructor() {
webidl.illegalConstructor();
}
/** See https://w3c.github.io/ServiceWorker/#dom-cache-put */
async put(request, response) {
webidl.assertBranded(this, CachePrototype);
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(",");
for (let i = 0; i < fieldValues.length; ++i) {
const field = fieldValues[i];
if (field.trim() === "*") {
throw new TypeError("Vary header must not contain '*'");
}
}
}
// Step 8.
if (innerResponse.body !== null && innerResponse.body.unusable()) {
throw new TypeError("Response body is already used");
}
// acquire lock before async op
const reader = innerResponse.body?.stream.getReader();
// 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 (reader) {
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
await core.shutdown(rid);
break;
}
await core.writeAll(rid, value);
}
} finally {
core.close(rid);
}
}
// Step 12-19: TODO(@satyarohith): do the insertion in background.
}
/** See https://w3c.github.io/ServiceWorker/#cache-match */
async match(request, options) {
webidl.assertBranded(this, CachePrototype);
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) {
webidl.assertBranded(this, CachePrototype);
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;
const CachePrototype = Cache.prototype;
let cacheStorage;
window.__bootstrap.caches = {
CacheStorage,
Cache,
cacheStorage() {
if (!cacheStorage) {
cacheStorage = webidl.createBranded(CacheStorage);
}
return cacheStorage;
},
};
})(this);