-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathlegacy.ts
165 lines (136 loc) · 4.24 KB
/
legacy.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import chromeP from 'webext-polyfill-kinda';
import {isBackgroundPage, isExtensionContext} from 'webext-detect-page';
import toMilliseconds, {type TimeDescriptor} from '@sindresorhus/to-milliseconds';
const cacheDefault = {days: 30};
export function timeInTheFuture(time: TimeDescriptor): number {
return Date.now() + toMilliseconds(time);
}
type Primitive = boolean | number | string;
type Value = Primitive | Primitive[] | Record<string, any>;
// No circular references: Record<string, Value> https://github.com/Microsoft/TypeScript/issues/14174
// No index signature: {[key: string]: Value} https://github.com/microsoft/TypeScript/issues/15300#issuecomment-460226926
type CachedValue<Value> = {
data: Value;
maxAge: number;
};
type Cache<ScopedValue extends Value = Value> = Record<string, CachedValue<ScopedValue>>;
async function has(key: string): Promise<boolean> {
return (await _get(key, false)) !== undefined;
}
export async function _get<ScopedValue extends Value>(
key: string,
remove: boolean,
): Promise<CachedValue<ScopedValue> | undefined> {
const internalKey = `cache:${key}`;
const storageData = await chromeP.storage.local.get(internalKey) as Cache<ScopedValue>;
const cachedItem = storageData[internalKey];
if (cachedItem === undefined) {
// `undefined` means not in cache
return;
}
if (Date.now() > cachedItem.maxAge) {
if (remove) {
await chromeP.storage.local.remove(internalKey);
}
return;
}
return cachedItem;
}
async function get<ScopedValue extends Value>(
key: string,
): Promise<ScopedValue | undefined> {
const cachedValue = await _get<ScopedValue>(key, true);
return cachedValue?.data;
}
async function set<ScopedValue extends Value>(
key: string,
value: ScopedValue,
maxAge: TimeDescriptor = cacheDefault,
): Promise<ScopedValue> {
if (arguments.length < 2) {
throw new TypeError('Expected a value as the second argument');
}
if (value === undefined) {
await delete_(key);
} else {
const internalKey = `cache:${key}`;
await chromeP.storage.local.set({
[internalKey]: {
data: value,
maxAge: timeInTheFuture(maxAge),
},
});
}
return value;
}
async function delete_(userKey: string): Promise<void> {
const internalKey = `cache:${userKey}`;
return chromeP.storage.local.remove(internalKey);
}
async function deleteWithLogic(
logic?: (x: CachedValue<Value>) => boolean,
): Promise<void> {
const wholeCache = (await chromeP.storage.local.get()) as Record<string, any>;
const removableItems: string[] = [];
for (const [key, value] of Object.entries(wholeCache)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- TODO: value is any
if (key.startsWith('cache:') && (logic?.(value) ?? true)) {
removableItems.push(key);
}
}
if (removableItems.length > 0) {
await chromeP.storage.local.remove(removableItems);
}
}
async function deleteExpired(): Promise<void> {
await deleteWithLogic(cachedItem => Date.now() > cachedItem.maxAge);
}
async function clear(): Promise<void> {
await deleteWithLogic();
}
export type CacheKey<Arguments extends unknown[]> = (args: Arguments) => string;
export type MemoizedFunctionOptions<Arguments extends unknown[], ScopedValue> = {
maxAge?: TimeDescriptor;
staleWhileRevalidate?: TimeDescriptor;
cacheKey?: CacheKey<Arguments>;
shouldRevalidate?: (cachedValue: ScopedValue) => boolean;
};
/** @deprecated Use CachedValue and CachedFunction instead */
const cache = {
has,
get,
set,
clear,
delete: delete_,
};
function init(): void {
// Make it available globally for ease of use
if (isExtensionContext()) {
(globalThis as any).webextStorageCache = cache;
}
// Automatically clear cache every day
if (!isBackgroundPage()) {
return;
}
if (chrome.alarms) {
void chrome.alarms.create('webext-storage-cache', {
delayInMinutes: 1,
periodInMinutes: 60 * 24,
});
let lastRun = 0; // Homemade debouncing due to `chrome.alarms` potentially queueing this function
chrome.alarms.onAlarm.addListener(alarm => {
if (
alarm.name === 'webext-storage-cache'
&& lastRun < Date.now() - 1000
) {
lastRun = Date.now();
void deleteExpired();
}
});
} else {
setTimeout(deleteExpired, 60_000); // Purge cache on launch, but wait a bit
setInterval(deleteExpired, 1000 * 3600 * 24);
}
}
init();
export default cache;