2
2
* @copyright Copyright (c) 2022 Adam Josefus
3
3
*/
4
4
5
+ import { type DependenciesType } from "./DependenciesType.ts";
6
+
5
7
6
8
export type GeneratorType<T> = () => T;
7
- type LoadOnlyEntryType = [key: string];
8
- type LoadAndGenerateEntryType<T> = [key: string, generator: GeneratorType<T>];
9
- type LoadEntryType<T> = LoadOnlyEntryType | LoadAndGenerateEntryType<T>;
9
+
10
+ type LoadOnlyEntryType =
11
+ | [key: string]
12
+ | [key: string, dependencies: DependenciesType];
13
+
14
+ type LoadAndGenerateEntryType<T> =
15
+ | [key: string, generator: GeneratorType<T>]
16
+ | [key: string, generator: GeneratorType<T>, dependencies?: DependenciesType];
17
+
18
+ type LoadEntryType<T> =
19
+ | LoadOnlyEntryType | LoadAndGenerateEntryType<T>;
20
+
21
+ type StateType = {
22
+ timestamp: number;
23
+ files: Map<string, number>;
24
+ };
10
25
11
26
12
27
export class Cache<T> {
13
28
14
- readonly #storage: Map<string, T> = new Map();
29
+ readonly #storage: Map<string, {
30
+ value: T,
31
+ dependencies?: DependenciesType,
32
+ state: StateType,
33
+ }> = new Map();
15
34
16
35
17
36
/**
@@ -20,15 +39,46 @@ export class Cache<T> {
20
39
* @returns `<T>` or `<T> | undefined` – depending on whether the generator has been set.
21
40
*/
22
41
load<E extends LoadEntryType<T>>(...args: E): E extends LoadAndGenerateEntryType<T> ? T : T | undefined {
23
- const [key, generator] = args;
24
-
25
- if (this.#storage.has(key)) {
26
- return this.#storage.get(key)!;
42
+ const { key, generator, dependencies } = (() => {
43
+ const [key, a, b] = args;
44
+
45
+ if (key !== undefined && a !== undefined && b !== undefined) {
46
+ return {
47
+ key,
48
+ generator: a as GeneratorType<T>,
49
+ dependencies: b as DependenciesType,
50
+ }
51
+
52
+ } else if (typeof a === 'function') {
53
+ return {
54
+ key,
55
+ generator: a as GeneratorType<T>,
56
+ dependencies: undefined,
57
+ }
58
+
59
+ } else if (typeof a === 'object') {
60
+ return {
61
+ key,
62
+ generator: undefined,
63
+ dependencies: a as DependenciesType,
64
+ }
65
+
66
+ } else {
67
+ return {
68
+ key,
69
+ generator: undefined,
70
+ dependencies: undefined,
71
+ }
72
+ }
73
+ })();
74
+
75
+ if (this.has(key)) {
76
+ return this.#load(key)!;
27
77
}
28
78
29
79
if (generator) {
30
80
const value = generator();
31
- this.save(key, value);
81
+ this.save(key, value, dependencies );
32
82
33
83
return value;
34
84
}
@@ -38,13 +88,27 @@ export class Cache<T> {
38
88
}
39
89
40
90
91
+ #load(key: string): T | undefined {
92
+ if (!this.#storage.has(key)) return undefined;
93
+
94
+ this.#update(key, true);
95
+
96
+ const { value } = this.#storage.get(key)!;
97
+ return value;
98
+ }
99
+
100
+
41
101
/**
42
102
* Save value to cache by key.
43
103
* @param key
44
104
* @param value
45
105
*/
46
- save(key: string, value: T): void {
47
- this.#storage.set(key, value);
106
+ save(key: string, value: T, dependencies?: DependenciesType): void {
107
+ this.#storage.set(key, {
108
+ value,
109
+ dependencies,
110
+ state: Cache.#createState(dependencies),
111
+ });
48
112
}
49
113
50
114
@@ -54,15 +118,101 @@ export class Cache<T> {
54
118
* @param value
55
119
*/
56
120
has(key: string): boolean {
121
+ this.#update(key, false);
122
+
57
123
return this.#storage.has(key);
58
124
}
59
125
60
126
61
127
/**
128
+ * Remove value from cache.
129
+ * @param key
130
+ */
131
+ remove(key: string): void {
132
+ this.#storage.delete(key);
133
+ }
134
+
135
+
136
+ /**
137
+ * @deprecated Use `remove` instead.
138
+ *
62
139
* Delete value from cache.
63
140
* @param key
64
141
*/
65
142
delete(key: string): void {
66
- this.#storage.delete(key);
143
+ this.remove(key);
144
+ }
145
+
146
+
147
+ #isValid(state: StateType, dependencies: DependenciesType) {
148
+ if (dependencies.expire) {
149
+ const expired = Date.now() > state.timestamp + dependencies.expire;
150
+ if (expired) return false;
151
+ }
152
+
153
+ if (dependencies.callbacks) {
154
+ const callbacks = [dependencies.callbacks].flat();
155
+ const invalid = callbacks.some(callback => !callback());
156
+
157
+ if (invalid) return false;
158
+ }
159
+
160
+ if (dependencies.files) {
161
+ const current = Cache.#computeFileModificationMap([dependencies.files].flat());
162
+
163
+ const invalid = [...state.files.entries()].some(([file, modifed]) => {
164
+ return !current.has(file) || current.get(file) !== modifed;
165
+ });
166
+
167
+ if (invalid) return false;
168
+ }
169
+
170
+ return true;
171
+ }
172
+
173
+
174
+ #update(key: string, refreshState: boolean): void {
175
+ if (!this.#storage.has(key)) return;
176
+
177
+ const { dependencies, state } = this.#storage.get(key)!;
178
+
179
+ if (!dependencies) return;
180
+
181
+ if (!this.#isValid(state, dependencies)) {
182
+ this.remove(key);
183
+ return
184
+ }
185
+
186
+ if (refreshState) {
187
+ if (dependencies.sliding) state.timestamp = Date.now();
188
+ }
189
+ }
190
+
191
+
192
+ static #createState(dependencies?: DependenciesType): StateType {
193
+ const files = [dependencies?.files ?? []].flat();
194
+
195
+ return {
196
+ timestamp: Date.now(),
197
+ files: Cache.#computeFileModificationMap(files),
198
+ };
199
+ }
200
+
201
+
202
+ static #computeFileModificationMap(files: string[]): Map<string, number> {
203
+ const result = new Map<string, number>();
204
+
205
+ files.forEach(file => {
206
+ try {
207
+ const modified = Deno.statSync(file).mtime?.getTime() ?? null;
208
+ if (modified === null) return;
209
+
210
+ result.set(file, modified);
211
+ } catch (_err) {
212
+ return;
213
+ }
214
+ });
215
+
216
+ return result;
67
217
}
68
218
}
0 commit comments