-
Notifications
You must be signed in to change notification settings - Fork 168
/
cache.ts
149 lines (137 loc) · 4.59 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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { computeHash, isFilledString, trimSuffix, utf8Dec } from "../shared/util.ts";
import { path } from "./deps.ts";
import { existsDir, existsFile } from "./helpers.ts";
import log from "./log.ts";
type CacheMeta = {
url: string;
headers: Record<string, string>;
now: {
secs_since_epoch: number;
nanos_since_epoch: number;
};
};
const memoryCache = new Map<string, [content: Uint8Array, meta: CacheMeta]>();
const reloaded = new Set<string>();
if (typeof Deno.run === "function") {
const p = Deno.run({
cmd: [Deno.execPath(), "info", "--json"],
stdout: "piped",
stderr: "null",
});
const output = utf8Dec.decode(await p.output());
const { modulesCache } = JSON.parse(output);
if (isFilledString(modulesCache)) {
Deno.env.set("MODULES_CACHE_DIR", modulesCache);
}
p.close();
}
/** fetch and cache remote contents */
export async function cacheFetch(
url: string,
options?: { forceRefresh?: boolean; retryTimes?: number; userAgent?: string },
): Promise<Response> {
const urlObj = new URL(url);
const { protocol, hostname, port, pathname, searchParams } = urlObj;
const isLocalhost = ["localhost", "0.0.0.0", "127.0.0.1"].includes(hostname);
const modulesCacheDir = Deno.env.get("MODULES_CACHE_DIR");
let cacheKey = "";
let cacheDir = "";
let metaFilepath = "";
let contentFilepath = "";
if (!isLocalhost) {
searchParams.delete("v");
searchParams.sort();
url = urlObj.toString();
cacheKey = await computeHash("sha-256", pathname + searchParams.toString() + (options?.userAgent || ""));
}
if (modulesCacheDir) {
cacheDir = path.join(modulesCacheDir, trimSuffix(protocol, ":"), hostname + (port ? "_PORT" + port : ""));
contentFilepath = path.join(cacheDir, cacheKey);
metaFilepath = path.join(cacheDir, cacheKey + ".metadata.json");
}
if (!options?.forceRefresh && !isLocalhost) {
if (modulesCacheDir) {
if (await existsFile(contentFilepath) && await existsFile(metaFilepath)) {
const shouldReload = Deno.env.get("ALEPH_RELOAD_FLAG");
if (!shouldReload || 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(cacheKey)) {
const [content, meta] = memoryCache.get(cacheKey)!;
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.info("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: CacheMeta = {
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(cacheKey, [content, meta]);
}
return new Response(content, { headers: res.headers });
}
return res;
}
return finalRes;
}
function isExpired(meta: CacheMeta) {
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;
}