|
1 | 1 | import type { AstroGlobal } from 'astro'; |
2 | | -import fs from 'node:fs'; |
| 2 | +import fs from 'node:fs/promises'; |
3 | 3 | import path from 'node:path'; |
4 | 4 |
|
5 | 5 | export class Cache { |
6 | 6 | private cacheDir: string; |
7 | 7 |
|
8 | 8 | constructor(cacheDir: string) { |
9 | 9 | this.cacheDir = cacheDir; |
10 | | - if (!fs.existsSync(cacheDir)) { |
11 | | - fs.mkdirSync(cacheDir, { recursive: true }); |
12 | | - } |
| 10 | + } |
| 11 | + |
| 12 | + private async ensureDir(): Promise<void> { |
| 13 | + await fs.mkdir(this.cacheDir, { recursive: true }); |
13 | 14 | } |
14 | 15 |
|
15 | 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
16 | | - set(key: string, data: any, expiresIn?: number): void { |
17 | | - // Ensure cache directory exists in case it was deleted while the dev server is running |
18 | | - if (!fs.existsSync(this.cacheDir)) { |
19 | | - fs.mkdirSync(this.cacheDir, { recursive: true }); |
20 | | - } |
| 17 | + async set(key: string, data: any, expiresIn?: number): Promise<void> { |
| 18 | + await this.ensureDir(); |
21 | 19 |
|
22 | 20 | const filePath = path.join(this.cacheDir, `${key}.json`); |
23 | | - fs.writeFileSync(filePath, JSON.stringify(data), 'utf-8'); |
| 21 | + await fs.writeFile(filePath, JSON.stringify(data), 'utf-8'); |
24 | 22 |
|
25 | 23 | if (expiresIn) { |
26 | 24 | const expirationTime = Date.now() + expiresIn; |
27 | | - fs.writeFileSync( |
| 25 | + await fs.writeFile( |
28 | 26 | filePath.replace('.json', '.expires'), |
29 | 27 | JSON.stringify(expirationTime), |
30 | 28 | 'utf-8' |
31 | 29 | ); |
32 | 30 | } |
33 | 31 | } |
34 | 32 |
|
35 | | - get<T>(key: string): T | null { |
| 33 | + async get<T>(key: string): Promise<T | null> { |
36 | 34 | const filePath = path.join(this.cacheDir, `${key}.json`); |
37 | 35 | const expiresPath = path.join(this.cacheDir, `${key}.expires`); |
38 | 36 |
|
39 | | - if (fs.existsSync(filePath)) { |
40 | | - try { |
41 | | - const data = fs.readFileSync(filePath, 'utf-8'); |
| 37 | + try { |
| 38 | + const data = await fs.readFile(filePath, 'utf-8'); |
42 | 39 |
|
43 | | - // Check if file is empty or whitespace only |
44 | | - if (!data.trim()) { |
45 | | - console.warn(`Cache file for key "${key}" is empty, clearing cache`); |
46 | | - this.clear(key); |
47 | | - return null; |
48 | | - } |
| 40 | + if (!data.trim()) { |
| 41 | + console.warn(`Cache file for key "${key}" is empty, clearing cache`); |
| 42 | + await this.clear(key); |
| 43 | + return null; |
| 44 | + } |
49 | 45 |
|
50 | | - let expirationTime: number | null = null; |
51 | | - if (fs.existsSync(expiresPath)) { |
52 | | - try { |
53 | | - const expiresData = fs.readFileSync(expiresPath, 'utf-8'); |
54 | | - expirationTime = JSON.parse(expiresData); |
55 | | - } catch (error) { |
56 | | - console.warn(`Invalid expiration file for key "${key}", clearing cache:`, error); |
57 | | - this.clear(key); |
58 | | - return null; |
59 | | - } |
60 | | - } |
| 46 | + let expirationTime: number | null = null; |
| 47 | + try { |
| 48 | + const expiresData = await fs.readFile(expiresPath, 'utf-8'); |
| 49 | + expirationTime = JSON.parse(expiresData); |
| 50 | + } catch { |
| 51 | + // No expires file or invalid — treat as no expiry |
| 52 | + } |
61 | 53 |
|
62 | | - if (expirationTime && Date.now() > expirationTime) { |
63 | | - this.clear(key); |
64 | | - return null; |
65 | | - } |
| 54 | + if (expirationTime && Date.now() > expirationTime) { |
| 55 | + await this.clear(key); |
| 56 | + return null; |
| 57 | + } |
66 | 58 |
|
67 | | - return JSON.parse(data) as T; |
68 | | - } catch (error) { |
69 | | - console.warn(`Failed to parse cache file for key "${key}":`, error); |
70 | | - this.clear(key); |
| 59 | + return JSON.parse(data) as T; |
| 60 | + } catch (err) { |
| 61 | + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
71 | 62 | return null; |
72 | 63 | } |
| 64 | + console.warn(`Failed to parse cache file for key "${key}":`, err); |
| 65 | + await this.clear(key); |
| 66 | + return null; |
73 | 67 | } |
74 | | - return null; |
75 | 68 | } |
76 | 69 |
|
77 | | - clear(key: string): void { |
| 70 | + async clear(key: string): Promise<void> { |
78 | 71 | const filePath = path.join(this.cacheDir, `${key}.json`); |
79 | 72 | const expiresPath = path.join(this.cacheDir, `${key}.expires`); |
80 | | - if (fs.existsSync(filePath)) { |
81 | | - fs.unlinkSync(filePath); |
| 73 | + try { |
| 74 | + await fs.unlink(filePath); |
| 75 | + } catch { |
| 76 | + // Ignore ENOENT |
82 | 77 | } |
83 | | - if (fs.existsSync(expiresPath)) { |
84 | | - fs.unlinkSync(expiresPath); |
| 78 | + try { |
| 79 | + await fs.unlink(expiresPath); |
| 80 | + } catch { |
| 81 | + // Ignore ENOENT |
85 | 82 | } |
86 | 83 | } |
87 | 84 |
|
88 | | - clearAll(): void { |
89 | | - const files = fs.readdirSync(this.cacheDir); |
90 | | - for (const file of files) { |
91 | | - fs.unlinkSync(path.join(this.cacheDir, file)); |
| 85 | + async clearAll(): Promise<void> { |
| 86 | + try { |
| 87 | + const files = await fs.readdir(this.cacheDir); |
| 88 | + await Promise.all(files.map((file) => fs.unlink(path.join(this.cacheDir, file)))); |
| 89 | + } catch (err) { |
| 90 | + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { |
| 91 | + throw err; |
| 92 | + } |
92 | 93 | } |
93 | 94 | } |
94 | 95 |
|
95 | | - has(key: string): boolean { |
| 96 | + async has(key: string): Promise<boolean> { |
96 | 97 | const filePath = path.join(this.cacheDir, `${key}.json`); |
97 | 98 | const expiresPath = path.join(this.cacheDir, `${key}.expires`); |
98 | 99 |
|
99 | | - if (fs.existsSync(filePath)) { |
| 100 | + try { |
| 101 | + const data = await fs.readFile(filePath, 'utf-8'); |
| 102 | + if (!data.trim()) { |
| 103 | + await this.clear(key); |
| 104 | + return false; |
| 105 | + } |
| 106 | + |
100 | 107 | try { |
101 | | - // Check if file is empty |
102 | | - const data = fs.readFileSync(filePath, 'utf-8'); |
103 | | - if (!data.trim()) { |
104 | | - this.clear(key); |
| 108 | + const expiresData = await fs.readFile(expiresPath, 'utf-8'); |
| 109 | + const expirationTime = JSON.parse(expiresData); |
| 110 | + if (Date.now() > expirationTime) { |
| 111 | + await this.clear(key); |
105 | 112 | return false; |
106 | 113 | } |
107 | | - |
108 | | - if (fs.existsSync(expiresPath)) { |
109 | | - try { |
110 | | - const expirationTime = JSON.parse(fs.readFileSync(expiresPath, 'utf-8')); |
111 | | - if (Date.now() > expirationTime) { |
112 | | - this.clear(key); |
113 | | - return false; |
114 | | - } |
115 | | - } catch (error) { |
116 | | - console.warn(`Invalid expiration file for key "${key}", clearing cache:`, error); |
117 | | - this.clear(key); |
118 | | - return false; |
119 | | - } |
120 | | - } |
121 | | - return true; |
122 | | - } catch (error) { |
123 | | - console.warn(`Error checking cache for key "${key}":`, error); |
124 | | - this.clear(key); |
| 114 | + } catch { |
| 115 | + // No expires file — treat as valid |
| 116 | + } |
| 117 | + return true; |
| 118 | + } catch (err) { |
| 119 | + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
125 | 120 | return false; |
126 | 121 | } |
| 122 | + console.warn(`Error checking cache for key "${key}":`, err); |
| 123 | + await this.clear(key); |
| 124 | + return false; |
127 | 125 | } |
128 | | - return false; |
129 | 126 | } |
130 | 127 |
|
131 | | - getAllKeys(): string[] { |
132 | | - const files = fs.readdirSync(this.cacheDir); |
133 | | - return files.map((file) => path.basename(file, '.json')); |
| 128 | + async getAllKeys(): Promise<string[]> { |
| 129 | + try { |
| 130 | + const files = await fs.readdir(this.cacheDir); |
| 131 | + return files.map((file) => path.basename(file, '.json')); |
| 132 | + } catch (err) { |
| 133 | + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
| 134 | + return []; |
| 135 | + } |
| 136 | + throw err; |
| 137 | + } |
134 | 138 | } |
135 | 139 |
|
136 | 140 | setCacheHeader(type: 'long' | 'short') { |
|
0 commit comments