-
Notifications
You must be signed in to change notification settings - Fork 1
/
cache.ts
94 lines (86 loc) · 3.28 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
import type { UnixTime } from "./deps/scrapbox.ts";
import { findLatestCache } from "./deps/scrapbox-std-browser.ts";
import { makeThrottle } from "./throttle.ts";
import { delay } from "./deps/async.ts";
const cacheVersion = "0.6.5"; // release前に更新する
const cacheName = `ScrapBubble-${cacheVersion}`;
const cache = await globalThis.caches.open(cacheName);
// 古いcacheがあったら削除しておく
// 他の操作をブロックする必要はない
(async () => {
for (const name of await globalThis.caches.keys()) {
if (name.startsWith("ScrapBubble-") && name !== cacheName) {
await globalThis.caches.delete(name);
console.log(`[ScrapBubble] deleted old cache :"${name}"`);
}
}
})();
/** 同時に3つまでfetchできるようにする函数 */
const throttle = makeThrottle<Response>(3);
export interface CacheFirstFetchOptions extends CacheQueryOptions {
/** 失敗したresponseをcacheに保存するかどうか
*
* @default `false` (保存しない)
*/
saveFailedResponse?: boolean;
}
/** cacheとnetworkから順番にresponseをとってくる
*
* networkからとってこなくていいときは、途中でiteratorをbreakすればfetchしない
*
* fetchは同時に3回までしかできないよう制限する
*
* @param req 要求
* @param options cacheを探すときの設定
*/
export async function* cacheFirstFetch(
req: Request,
options?: CacheFirstFetchOptions,
): AsyncGenerator<readonly ["cache" | "network", Response], void, unknown> {
// cacheから返す
// まず自前のcache storageから失敗したresponseを取得し、なければscrapbox.ioのcacheから正常なresponseを探す
const cachePromise =
((options?.saveFailedResponse ? cache.match(req) : undefined) ??
findLatestCache(req, options)).then((res) => ["cache", res] as const);
{
const timer = delay(1000).then(() => "timeout" as const);
const first = await Promise.race([cachePromise, timer]);
if (first !== "timeout") {
// 1秒以内にcacheを読み込めたら、cache→networkの順に返す
if (first[1]) {
yield ["cache", first[1]];
}
// networkから返す
const res = await throttle(() => fetch(req));
if (!res.ok && options?.saveFailedResponse) {
// scrapbox.ioは失敗したresponseをcacheしないので、自前のcacheに格納しておく
await cache.put(req, res.clone());
}
yield ["network", res];
}
}
// 1秒経ってもcacheの読み込みが終わらないときは、fetchを走らせ始める
const networkPromise = throttle(() => fetch(req))
.then((res) => ["network", res] as const);
const [type, res] = await Promise.race([cachePromise, networkPromise]);
if (type === "network") {
yield [type, res];
// networkPromiseが先にresolveしたら、cachePromiseはyieldせずに捨てる
return;
}
if (res) yield [type, res];
yield await networkPromise;
}
/** 有効期限切れのresponseかどうか調べる
*
* @param response 調べるresponse
* @param maxAge 寿命(単位はs)
*/
export const isExpired = (
response: Response,
maxAge: UnixTime,
): boolean => {
const updated = new Date(response.headers.get("Date") ?? 0).getTime() /
1000;
return updated + maxAge < new Date().getTime() / 1000;
};