Skip to content

Commit 931d799

Browse files
authored
Merge pull request #6 from adamjosefus/development
Add cache item dependencies for invalidation
2 parents 8d1755b + ca91b9a commit 931d799

File tree

4 files changed

+379
-12
lines changed

4 files changed

+379
-12
lines changed

Diff for: libs/Cache.ts

+162-12
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,35 @@
22
* @copyright Copyright (c) 2022 Adam Josefus
33
*/
44

5+
import { type DependenciesType } from "./DependenciesType.ts";
6+
57

68
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+
};
1025

1126

1227
export class Cache<T> {
1328

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();
1534

1635

1736
/**
@@ -20,15 +39,46 @@ export class Cache<T> {
2039
* @returns `<T>` or `<T> | undefined` – depending on whether the generator has been set.
2140
*/
2241
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)!;
2777
}
2878

2979
if (generator) {
3080
const value = generator();
31-
this.save(key, value);
81+
this.save(key, value, dependencies);
3282

3383
return value;
3484
}
@@ -38,13 +88,27 @@ export class Cache<T> {
3888
}
3989

4090

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+
41101
/**
42102
* Save value to cache by key.
43103
* @param key
44104
* @param value
45105
*/
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+
});
48112
}
49113

50114

@@ -54,15 +118,101 @@ export class Cache<T> {
54118
* @param value
55119
*/
56120
has(key: string): boolean {
121+
this.#update(key, false);
122+
57123
return this.#storage.has(key);
58124
}
59125

60126

61127
/**
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+
*
62139
* Delete value from cache.
63140
* @param key
64141
*/
65142
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;
67217
}
68218
}

Diff for: libs/DependenciesType.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @copyright Copyright (c) 2022 Adam Josefus
3+
*/
4+
5+
6+
export type DependenciesType = {
7+
// /**
8+
// * Set priority of the cashed value.
9+
// */
10+
// priority?: number;
11+
/**
12+
* Expired time in milliseconds.
13+
*/
14+
expire?: number,
15+
/**
16+
* If callback return false, the cache is invalidated.
17+
*/
18+
callbacks?: (() => boolean) | (() => boolean)[],
19+
/**
20+
* If files are changed, the cache is invalidated.
21+
*/
22+
files?: string | string[],
23+
/**
24+
* If true, extends the validity period with each reading.
25+
*/
26+
sliding?: boolean,
27+
};

Diff for: mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './libs/Cache.ts';
2+
export * from './libs/DependenciesType.ts';

0 commit comments

Comments
 (0)