forked from camptocamp/ogc-client
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcache.ts
More file actions
142 lines (131 loc) · 3.9 KB
/
cache.ts
File metadata and controls
142 lines (131 loc) · 3.9 KB
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
let cacheExpiryDuration = 1000 * 60 * 60; // 1 day
/**
* Sets a new cache expiry duration, in ms.
* Setting this to a value <= 0 will disable the caching logic altogether
* and not store cache entries at all
* @param value Duration in ms
*/
export function setCacheExpiryDuration(value: number) {
cacheExpiryDuration = value;
}
/**
* Returns the current cache expiry duration in ms
*/
export function getCacheExpiryDuration() {
return cacheExpiryDuration;
}
let cachePromise: Promise<Cache | null>;
export function getCache() {
if (cachePromise !== undefined) return cachePromise;
if (!('caches' in globalThis)) {
cachePromise = Promise.resolve(null);
return cachePromise;
}
cachePromise = caches.open('ogc-client').catch((e) => {
console.info(
'[ogc-client] Cache could not be accessed for the following reason:',
e
);
return null;
});
return cachePromise;
}
// use only in tests
export function _resetCache() {
cachePromise = undefined;
}
export async function storeCacheEntry(object: unknown, ...keys: string[]) {
const cache = await getCache();
if (!cache) return;
const entryUrl = 'https://cache/' + keys.join('/');
try {
await cache.put(
entryUrl,
new Response(JSON.stringify(object), {
headers: {
'x-expiry': (Date.now() + getCacheExpiryDuration()).toString(10),
},
})
);
} catch (e) {
console.info(
'[ogc-client] Caching failed once for the following reason and will not be retried:',
e
);
cachePromise = Promise.resolve(null); // this will disable caching
}
}
export async function hasValidCacheEntry(...keys: string[]) {
const cache = await getCache();
if (!cache) return;
const entryUrl = 'https://cache/' + keys.join('/');
return cache
.match(entryUrl)
.then((req) => !!req && parseInt(req.headers.get('x-expiry')) > Date.now());
}
export async function readCacheEntry(...keys: string[]) {
const cache = await getCache();
if (!cache) return;
const entryUrl = 'https://cache/' + keys.join('/');
const response = await cache.match(entryUrl);
return response ? response.clone().json() : null;
}
/**
* Map of task promises; when a promise resolves the map entry is cleared
*/
const tasksMap: Map<string, Promise<unknown>> = new Map();
/**
* This will skip a long/expensive task and use a cached value if available,
* otherwise the task will be run normally
* Note: outside of a browser's main thread, caching will never happen!
* @param factory A function encapsulating
* the long/expensive task; non-serializable properties of the returned object
* will be set to null
* @param keys Keys will be concatenated for storage
* @return Resolves to either a cached object or a fresh one
*/
export async function useCache<T>(
factory: () => T | Promise<T>,
...keys: string[]
): Promise<T> {
await purgeOutdatedEntries();
if (await hasValidCacheEntry(...keys)) {
return readCacheEntry(...keys);
}
const taskKey = keys.join('#');
if (tasksMap.has(taskKey)) {
return tasksMap.get(taskKey) as T;
}
const taskRun = factory();
if (taskRun instanceof Promise) {
taskRun.then(() => tasksMap.delete(taskKey));
tasksMap.set(taskKey, taskRun);
}
const result = await taskRun;
await storeCacheEntry(result, ...keys);
return result;
}
/**
* Removes all expired entries from the cache
*/
export async function purgeOutdatedEntries() {
const cache = await getCache();
if (!cache) return;
const keys = await cache.keys();
for (const key of keys) {
const resp = await cache.match(key);
if (parseInt(resp.headers.get('x-expiry')) <= Date.now())
await cache.delete(key);
}
}
/**
* Remove all cache entries; will not prevent the creation of new ones
*/
export async function clearCache() {
const cache = await getCache();
if (!cache) return;
const keys = await cache.keys();
for (const key of keys) {
await cache.delete(key);
}
}