/
cache.ts
128 lines (117 loc) · 4.02 KB
/
cache.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { join } from "https://deno.land/std@0.136.0/path/mod.ts";
import { existsDir, existsFile } from "./fs.ts";
import log from "./log.ts";
import util from "./util.ts";
type Meta = {
url: string;
headers: Record<string, string>;
now: {
secs_since_epoch: number;
nanos_since_epoch: number;
};
};
const memoryCache = new Map<string, [content: Uint8Array, meta: Meta]>();
const reloaded = new Set<string>();
/** fetch and cache remote contents */
export default async function cache(
url: string,
options?: { forceRefresh?: boolean; retryTimes?: number; userAgent?: string },
): Promise<Response> {
const { protocol, hostname, port, pathname, search } = new URL(url);
const isLocalhost = ["localhost", "0.0.0.0", "127.0.0.1"].includes(hostname);
const modulesCacheDir = Deno.env.get("MODULES_CACHE_DIR");
const hashname = isLocalhost ? "" : await util.computeHash("sha-256", pathname + search + (options?.userAgent || ""));
let cacheDir = "";
let metaFilepath = "";
let contentFilepath = "";
if (modulesCacheDir) {
cacheDir = join(modulesCacheDir, util.trimSuffix(protocol, ":"), hostname + (port ? "_PORT" + port : ""));
contentFilepath = join(cacheDir, hashname);
metaFilepath = join(cacheDir, hashname + ".metadata.json");
}
if (!options?.forceRefresh && !isLocalhost) {
if (modulesCacheDir) {
if (await existsFile(contentFilepath) && await existsFile(metaFilepath)) {
const reload = Deno.env.get("ALEPH_RELOAD_FLAG");
if (!reload || reloaded.has(url)) {
const [content, metaJSON] = await Promise.all([
Deno.readFile(contentFilepath),
Deno.readTextFile(metaFilepath),
]);
try {
const meta = JSON.parse(metaJSON);
if (!isExpired(meta)) {
return new Response(content, { headers: { ...meta.headers, "cache-hit": "true" } });
}
} catch (_e) {
log.debug(`skip cache of ${url}: invalid cache metadata file`);
}
} else {
reloaded.add(url);
}
}
} else if (memoryCache.has(hashname)) {
const [content, meta] = memoryCache.get(hashname)!;
if (!isExpired(meta)) {
return new Response(content, { headers: { ...meta.headers, "cache-hit": "true" } });
}
}
}
const retryTimes = options?.retryTimes ?? 3;
let finalRes = new Response("Server Error", { status: 500 });
for (let i = 0; i < retryTimes; i++) {
if (i === 0) {
if (!isLocalhost) {
log.debug("Download", url);
}
} else {
log.warn(`Download ${url} failed, retrying...`);
}
const res = await fetch(url, { headers: options?.userAgent ? { "User-Agent": options?.userAgent } : undefined });
if (res.status >= 500) {
finalRes = res;
continue;
}
if (res.ok && !isLocalhost) {
const buffer = await res.arrayBuffer();
const content = new Uint8Array(buffer);
const meta: Meta = {
url,
headers: {},
now: {
secs_since_epoch: Math.round(Date.now() / 1000),
nanos_since_epoch: 0,
},
};
res.headers.forEach((val, key) => {
meta.headers[key] = val;
});
if (modulesCacheDir) {
if (!(await existsDir(cacheDir))) {
await Deno.mkdir(cacheDir, { recursive: true });
}
await Promise.all([
Deno.writeFile(contentFilepath, content),
Deno.writeTextFile(metaFilepath, JSON.stringify(meta, undefined, 2)),
]);
} else {
memoryCache.set(hashname, [content, meta]);
}
return new Response(content, { headers: res.headers });
}
return res;
}
return finalRes;
}
function isExpired(meta: Meta) {
const cc = meta.headers["cache-control"];
const dataCacheTtl = cc && cc.includes("max-age=") ? parseInt(cc.split("max-age=")[1]) : undefined;
if (dataCacheTtl) {
const now = Date.now();
const expireTime = (meta.now.secs_since_epoch + dataCacheTtl) * 1000;
if (now > expireTime) {
return true;
}
}
return false;
}