@nahisaho/shikigami-mcp-server 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/cache/__tests__/global.test.d.ts +6 -0
- package/dist/cache/__tests__/global.test.d.ts.map +1 -0
- package/dist/cache/__tests__/global.test.js +269 -0
- package/dist/cache/__tests__/global.test.js.map +1 -0
- package/dist/cache/__tests__/manager.test.d.ts +6 -0
- package/dist/cache/__tests__/manager.test.d.ts.map +1 -0
- package/dist/cache/__tests__/manager.test.js +286 -0
- package/dist/cache/__tests__/manager.test.js.map +1 -0
- package/dist/cache/__tests__/semantic.test.d.ts +6 -0
- package/dist/cache/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/cache/__tests__/semantic.test.js +271 -0
- package/dist/cache/__tests__/semantic.test.js.map +1 -0
- package/dist/cache/__tests__/store.test.d.ts +6 -0
- package/dist/cache/__tests__/store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/store.test.js +289 -0
- package/dist/cache/__tests__/store.test.js.map +1 -0
- package/dist/cache/global.d.ts +140 -0
- package/dist/cache/global.d.ts.map +1 -0
- package/dist/cache/global.js +260 -0
- package/dist/cache/global.js.map +1 -0
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +10 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/manager.d.ts +146 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +229 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cache/semantic.d.ts +164 -0
- package/dist/cache/semantic.d.ts.map +1 -0
- package/dist/cache/semantic.js +241 -0
- package/dist/cache/semantic.js.map +1 -0
- package/dist/cache/store.d.ts +98 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +469 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cache/types.d.ts +171 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +8 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/config/types.d.ts +67 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +30 -0
- package/dist/config/types.js.map +1 -1
- package/dist/tools/__tests__/multilingual-search.test.d.ts +7 -0
- package/dist/tools/__tests__/multilingual-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/multilingual-search.test.js +71 -0
- package/dist/tools/__tests__/multilingual-search.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js +249 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js +158 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js.map +1 -0
- package/dist/tools/search/recovery/index.d.ts +31 -2
- package/dist/tools/search/recovery/index.d.ts.map +1 -1
- package/dist/tools/search/recovery/index.js +51 -7
- package/dist/tools/search/recovery/index.js.map +1 -1
- package/dist/tools/search/recovery/logger.d.ts +149 -0
- package/dist/tools/search/recovery/logger.d.ts.map +1 -0
- package/dist/tools/search/recovery/logger.js +218 -0
- package/dist/tools/search/recovery/logger.js.map +1 -0
- package/dist/tools/search.d.ts +48 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +152 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts +10 -0
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js +239 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts +8 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js +271 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js.map +1 -0
- package/dist/tools/visit/recovery/index.d.ts +126 -0
- package/dist/tools/visit/recovery/index.d.ts.map +1 -0
- package/dist/tools/visit/recovery/index.js +203 -0
- package/dist/tools/visit/recovery/index.js.map +1 -0
- package/dist/tools/visit/recovery/wayback.d.ts +101 -0
- package/dist/tools/visit/recovery/wayback.d.ts.map +1 -0
- package/dist/tools/visit/recovery/wayback.js +140 -0
- package/dist/tools/visit/recovery/wayback.js.map +1 -0
- package/dist/tools/visit.d.ts +33 -0
- package/dist/tools/visit.d.ts.map +1 -1
- package/dist/tools/visit.js +127 -1
- package/dist/tools/visit.js.map +1 -1
- package/package.json +7 -3
- package/shikigami.config.example.yaml +9 -0
- package/src/cache/__tests__/global.test.ts +340 -0
- package/src/cache/__tests__/manager.test.ts +353 -0
- package/src/cache/__tests__/semantic.test.ts +331 -0
- package/src/cache/__tests__/store.test.ts +369 -0
- package/src/cache/global.ts +351 -0
- package/src/cache/index.ts +10 -0
- package/src/cache/manager.ts +325 -0
- package/src/cache/semantic.ts +368 -0
- package/src/cache/store.ts +555 -0
- package/src/cache/types.ts +189 -0
- package/src/config/types.ts +108 -0
- package/src/tools/__tests__/multilingual-search.test.ts +88 -0
- package/src/tools/search/recovery/__tests__/logger.test.ts +334 -0
- package/src/tools/search/recovery/__tests__/manager-logger.test.ts +199 -0
- package/src/tools/search/recovery/index.ts +67 -9
- package/src/tools/search/recovery/logger.ts +351 -0
- package/src/tools/search.ts +212 -0
- package/src/tools/visit/recovery/__tests__/index.test.ts +297 -0
- package/src/tools/visit/recovery/__tests__/wayback.test.ts +344 -0
- package/src/tools/visit/recovery/index.ts +312 -0
- package/src/tools/visit/recovery/wayback.ts +210 -0
- package/src/tools/visit.ts +159 -2
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based Cache Store Implementation
|
|
3
|
+
* v1.0.0 - REQ-CACHE-001-01
|
|
4
|
+
*
|
|
5
|
+
* ファイルベースのキャッシュストア実装
|
|
6
|
+
* LRU削除ポリシー、TTL管理対応
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import {
|
|
13
|
+
ICacheStore,
|
|
14
|
+
CacheEntry,
|
|
15
|
+
CacheEntryMeta,
|
|
16
|
+
CacheResult,
|
|
17
|
+
CacheSetOptions,
|
|
18
|
+
CacheQueryOptions,
|
|
19
|
+
CacheStats,
|
|
20
|
+
CacheConfig,
|
|
21
|
+
CacheSource,
|
|
22
|
+
SourceCacheStats,
|
|
23
|
+
LruCandidate,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* デフォルトキャッシュ設定
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_CACHE_CONFIG: CacheConfig = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
maxEntries: 1000,
|
|
32
|
+
defaultTtlSeconds: 3600, // 1時間
|
|
33
|
+
maxSizeBytes: 100 * 1024 * 1024, // 100MB
|
|
34
|
+
cacheDir: '.shikigami/cache',
|
|
35
|
+
ttlBySource: {
|
|
36
|
+
search: 3600, // 1時間
|
|
37
|
+
visit: 86400, // 24時間
|
|
38
|
+
embedding: 604800, // 7日
|
|
39
|
+
analysis: 86400, // 24時間
|
|
40
|
+
other: 3600, // 1時間
|
|
41
|
+
},
|
|
42
|
+
useGlobalCache: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ファイルベースキャッシュストア
|
|
47
|
+
*/
|
|
48
|
+
export class FileCacheStore implements ICacheStore {
|
|
49
|
+
private config: CacheConfig;
|
|
50
|
+
private stats: CacheStats;
|
|
51
|
+
private initialized: boolean = false;
|
|
52
|
+
|
|
53
|
+
constructor(config: Partial<CacheConfig> = {}) {
|
|
54
|
+
this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
|
|
55
|
+
this.stats = this.initStats();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 統計情報を初期化
|
|
60
|
+
*/
|
|
61
|
+
private initStats(): CacheStats {
|
|
62
|
+
return {
|
|
63
|
+
totalEntries: 0,
|
|
64
|
+
totalSizeBytes: 0,
|
|
65
|
+
hits: 0,
|
|
66
|
+
misses: 0,
|
|
67
|
+
hitRate: 0,
|
|
68
|
+
expiredEvictions: 0,
|
|
69
|
+
lruEvictions: 0,
|
|
70
|
+
bySource: {},
|
|
71
|
+
statsStartedAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* キャッシュディレクトリを初期化
|
|
77
|
+
*/
|
|
78
|
+
private async ensureInitialized(): Promise<void> {
|
|
79
|
+
if (this.initialized) return;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await fs.mkdir(this.config.cacheDir, { recursive: true });
|
|
83
|
+
await fs.mkdir(path.join(this.config.cacheDir, 'data'), { recursive: true });
|
|
84
|
+
await fs.mkdir(path.join(this.config.cacheDir, 'meta'), { recursive: true });
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
|
|
87
|
+
// 既存の統計を読み込み
|
|
88
|
+
await this.loadStats();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// ディレクトリ作成エラーは無視(すでに存在する場合)
|
|
91
|
+
this.initialized = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* キーからファイルパスを生成
|
|
97
|
+
*/
|
|
98
|
+
private getFilePaths(key: string): { dataPath: string; metaPath: string } {
|
|
99
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
100
|
+
const subDir = hash.substring(0, 2); // 最初の2文字でサブディレクトリ分割
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
dataPath: path.join(this.config.cacheDir, 'data', subDir, `${hash}.json`),
|
|
104
|
+
metaPath: path.join(this.config.cacheDir, 'meta', subDir, `${hash}.json`),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* クエリからキャッシュキーを生成
|
|
110
|
+
*/
|
|
111
|
+
static generateKey(query: string, source: CacheSource = 'other'): string {
|
|
112
|
+
return `${source}:${query}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* クエリハッシュを生成
|
|
117
|
+
*/
|
|
118
|
+
static generateQueryHash(query: string): string {
|
|
119
|
+
return crypto.createHash('sha256').update(query).digest('hex').substring(0, 16);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* キャッシュエントリを取得
|
|
124
|
+
*/
|
|
125
|
+
async get<T = unknown>(key: string): Promise<CacheResult<T>> {
|
|
126
|
+
await this.ensureInitialized();
|
|
127
|
+
|
|
128
|
+
const { dataPath, metaPath } = this.getFilePaths(key);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// メタデータを読み込み
|
|
132
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
133
|
+
const meta: CacheEntryMeta = JSON.parse(metaContent);
|
|
134
|
+
|
|
135
|
+
// 期限切れチェック
|
|
136
|
+
if (new Date(meta.expiresAt) < new Date()) {
|
|
137
|
+
this.stats.misses++;
|
|
138
|
+
this.updateHitRate();
|
|
139
|
+
// 期限切れエントリを削除
|
|
140
|
+
await this.delete(key);
|
|
141
|
+
return { hit: false, key };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// データを読み込み
|
|
145
|
+
const dataContent = await fs.readFile(dataPath, 'utf-8');
|
|
146
|
+
const value = JSON.parse(dataContent) as T;
|
|
147
|
+
|
|
148
|
+
// アクセス情報を更新
|
|
149
|
+
meta.lastAccessedAt = new Date().toISOString();
|
|
150
|
+
meta.accessCount++;
|
|
151
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
152
|
+
|
|
153
|
+
// 統計更新
|
|
154
|
+
this.stats.hits++;
|
|
155
|
+
this.updateSourceStats(meta.source, 'hit');
|
|
156
|
+
this.updateHitRate();
|
|
157
|
+
|
|
158
|
+
return { hit: true, value, meta, key };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// ファイルが存在しない場合
|
|
161
|
+
this.stats.misses++;
|
|
162
|
+
this.updateHitRate();
|
|
163
|
+
return { hit: false, key };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* キャッシュエントリを保存
|
|
169
|
+
*/
|
|
170
|
+
async set<T = unknown>(key: string, value: T, options: CacheSetOptions = {}): Promise<void> {
|
|
171
|
+
await this.ensureInitialized();
|
|
172
|
+
|
|
173
|
+
const { dataPath, metaPath } = this.getFilePaths(key);
|
|
174
|
+
const source = options.source || 'other';
|
|
175
|
+
const ttlSeconds =
|
|
176
|
+
options.ttlSeconds || this.config.ttlBySource?.[source] || this.config.defaultTtlSeconds;
|
|
177
|
+
|
|
178
|
+
const now = new Date();
|
|
179
|
+
const dataStr = JSON.stringify(value);
|
|
180
|
+
const size = Buffer.byteLength(dataStr, 'utf-8');
|
|
181
|
+
|
|
182
|
+
const meta: CacheEntryMeta = {
|
|
183
|
+
createdAt: now.toISOString(),
|
|
184
|
+
lastAccessedAt: now.toISOString(),
|
|
185
|
+
expiresAt: new Date(now.getTime() + ttlSeconds * 1000).toISOString(),
|
|
186
|
+
accessCount: 0,
|
|
187
|
+
size,
|
|
188
|
+
source,
|
|
189
|
+
queryHash: FileCacheStore.generateQueryHash(key),
|
|
190
|
+
originalKey: key,
|
|
191
|
+
tags: options.tags,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// サイズ超過時はLRU削除
|
|
195
|
+
if (this.stats.totalSizeBytes + size > this.config.maxSizeBytes) {
|
|
196
|
+
await this.evictLru(size);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// エントリ数超過時はLRU削除
|
|
200
|
+
if (this.stats.totalEntries >= this.config.maxEntries) {
|
|
201
|
+
await this.evictLru(size);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ディレクトリ作成
|
|
205
|
+
await fs.mkdir(path.dirname(dataPath), { recursive: true });
|
|
206
|
+
await fs.mkdir(path.dirname(metaPath), { recursive: true });
|
|
207
|
+
|
|
208
|
+
// ファイル書き込み
|
|
209
|
+
await fs.writeFile(dataPath, dataStr);
|
|
210
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
211
|
+
|
|
212
|
+
// 統計更新
|
|
213
|
+
this.stats.totalEntries++;
|
|
214
|
+
this.stats.totalSizeBytes += size;
|
|
215
|
+
this.updateSourceStats(source, 'add', size);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* キャッシュエントリを削除
|
|
220
|
+
*/
|
|
221
|
+
async delete(key: string): Promise<boolean> {
|
|
222
|
+
await this.ensureInitialized();
|
|
223
|
+
|
|
224
|
+
const { dataPath, metaPath } = this.getFilePaths(key);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// メタデータを読んでサイズを取得
|
|
228
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
229
|
+
const meta: CacheEntryMeta = JSON.parse(metaContent);
|
|
230
|
+
|
|
231
|
+
// ファイル削除
|
|
232
|
+
await fs.unlink(dataPath).catch(() => {});
|
|
233
|
+
await fs.unlink(metaPath).catch(() => {});
|
|
234
|
+
|
|
235
|
+
// 統計更新
|
|
236
|
+
this.stats.totalEntries = Math.max(0, this.stats.totalEntries - 1);
|
|
237
|
+
this.stats.totalSizeBytes = Math.max(0, this.stats.totalSizeBytes - meta.size);
|
|
238
|
+
this.updateSourceStats(meta.source, 'remove', meta.size);
|
|
239
|
+
|
|
240
|
+
return true;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 全エントリを削除
|
|
248
|
+
*/
|
|
249
|
+
async clear(): Promise<void> {
|
|
250
|
+
await this.ensureInitialized();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await fs.rm(path.join(this.config.cacheDir, 'data'), { recursive: true, force: true });
|
|
254
|
+
await fs.rm(path.join(this.config.cacheDir, 'meta'), { recursive: true, force: true });
|
|
255
|
+
await fs.mkdir(path.join(this.config.cacheDir, 'data'), { recursive: true });
|
|
256
|
+
await fs.mkdir(path.join(this.config.cacheDir, 'meta'), { recursive: true });
|
|
257
|
+
} catch {
|
|
258
|
+
// 削除エラーは無視
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 統計リセット
|
|
262
|
+
this.stats = this.initStats();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* キーが存在するか確認
|
|
267
|
+
*/
|
|
268
|
+
async has(key: string): Promise<boolean> {
|
|
269
|
+
const result = await this.get(key);
|
|
270
|
+
return result.hit;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 統計情報を取得
|
|
275
|
+
*/
|
|
276
|
+
async getStats(): Promise<CacheStats> {
|
|
277
|
+
return { ...this.stats };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 期限切れエントリを削除
|
|
282
|
+
*/
|
|
283
|
+
async evictExpired(): Promise<number> {
|
|
284
|
+
await this.ensureInitialized();
|
|
285
|
+
|
|
286
|
+
let evictedCount = 0;
|
|
287
|
+
const metaDir = path.join(this.config.cacheDir, 'meta');
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const subDirs = await fs.readdir(metaDir);
|
|
291
|
+
|
|
292
|
+
for (const subDir of subDirs) {
|
|
293
|
+
const subDirPath = path.join(metaDir, subDir);
|
|
294
|
+
const stat = await fs.stat(subDirPath);
|
|
295
|
+
if (!stat.isDirectory()) continue;
|
|
296
|
+
|
|
297
|
+
const files = await fs.readdir(subDirPath);
|
|
298
|
+
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
if (!file.endsWith('.json')) continue;
|
|
301
|
+
|
|
302
|
+
const metaPath = path.join(subDirPath, file);
|
|
303
|
+
try {
|
|
304
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
305
|
+
const meta: CacheEntryMeta = JSON.parse(metaContent);
|
|
306
|
+
|
|
307
|
+
if (new Date(meta.expiresAt) < new Date()) {
|
|
308
|
+
const hash = file.replace('.json', '');
|
|
309
|
+
const dataPath = path.join(this.config.cacheDir, 'data', subDir, `${hash}.json`);
|
|
310
|
+
|
|
311
|
+
await fs.unlink(dataPath).catch(() => {});
|
|
312
|
+
await fs.unlink(metaPath).catch(() => {});
|
|
313
|
+
|
|
314
|
+
this.stats.totalEntries = Math.max(0, this.stats.totalEntries - 1);
|
|
315
|
+
this.stats.totalSizeBytes = Math.max(0, this.stats.totalSizeBytes - meta.size);
|
|
316
|
+
this.stats.expiredEvictions++;
|
|
317
|
+
evictedCount++;
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// ファイル読み込みエラーは無視
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// ディレクトリ読み込みエラーは無視
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return evictedCount;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* LRUポリシーでエントリを削除
|
|
333
|
+
*/
|
|
334
|
+
async evictLru(targetSizeBytes: number): Promise<number> {
|
|
335
|
+
await this.ensureInitialized();
|
|
336
|
+
|
|
337
|
+
// まず期限切れを削除
|
|
338
|
+
await this.evictExpired();
|
|
339
|
+
|
|
340
|
+
// まだサイズ超過なら古いものから削除
|
|
341
|
+
if (this.stats.totalSizeBytes + targetSizeBytes <= this.config.maxSizeBytes) {
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const candidates: LruCandidate[] = [];
|
|
346
|
+
const metaDir = path.join(this.config.cacheDir, 'meta');
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const subDirs = await fs.readdir(metaDir);
|
|
350
|
+
|
|
351
|
+
for (const subDir of subDirs) {
|
|
352
|
+
const subDirPath = path.join(metaDir, subDir);
|
|
353
|
+
const stat = await fs.stat(subDirPath);
|
|
354
|
+
if (!stat.isDirectory()) continue;
|
|
355
|
+
|
|
356
|
+
const files = await fs.readdir(subDirPath);
|
|
357
|
+
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
if (!file.endsWith('.json')) continue;
|
|
360
|
+
|
|
361
|
+
const metaPath = path.join(subDirPath, file);
|
|
362
|
+
try {
|
|
363
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
364
|
+
const meta: CacheEntryMeta = JSON.parse(metaContent);
|
|
365
|
+
const hash = file.replace('.json', '');
|
|
366
|
+
|
|
367
|
+
candidates.push({
|
|
368
|
+
key: `${subDir}/${hash}`,
|
|
369
|
+
lastAccessedAt: meta.lastAccessedAt,
|
|
370
|
+
size: meta.size,
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
// ファイル読み込みエラーは無視
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 古い順にソート
|
|
382
|
+
candidates.sort(
|
|
383
|
+
(a, b) => new Date(a.lastAccessedAt).getTime() - new Date(b.lastAccessedAt).getTime()
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
let evictedCount = 0;
|
|
387
|
+
let freedBytes = 0;
|
|
388
|
+
const targetFree = targetSizeBytes;
|
|
389
|
+
|
|
390
|
+
for (const candidate of candidates) {
|
|
391
|
+
if (freedBytes >= targetFree) break;
|
|
392
|
+
|
|
393
|
+
const [subDir, hash] = candidate.key.split('/');
|
|
394
|
+
const dataPath = path.join(this.config.cacheDir, 'data', subDir, `${hash}.json`);
|
|
395
|
+
const metaPath = path.join(this.config.cacheDir, 'meta', subDir, `${hash}.json`);
|
|
396
|
+
|
|
397
|
+
await fs.unlink(dataPath).catch(() => {});
|
|
398
|
+
await fs.unlink(metaPath).catch(() => {});
|
|
399
|
+
|
|
400
|
+
freedBytes += candidate.size;
|
|
401
|
+
evictedCount++;
|
|
402
|
+
this.stats.lruEvictions++;
|
|
403
|
+
this.stats.totalEntries = Math.max(0, this.stats.totalEntries - 1);
|
|
404
|
+
this.stats.totalSizeBytes = Math.max(0, this.stats.totalSizeBytes - candidate.size);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return evictedCount;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* エントリを検索
|
|
412
|
+
*/
|
|
413
|
+
async query(options: CacheQueryOptions): Promise<CacheEntry[]> {
|
|
414
|
+
await this.ensureInitialized();
|
|
415
|
+
|
|
416
|
+
const entries: CacheEntry[] = [];
|
|
417
|
+
const metaDir = path.join(this.config.cacheDir, 'meta');
|
|
418
|
+
const limit = options.limit || 100;
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const subDirs = await fs.readdir(metaDir);
|
|
422
|
+
|
|
423
|
+
for (const subDir of subDirs) {
|
|
424
|
+
if (entries.length >= limit) break;
|
|
425
|
+
|
|
426
|
+
const subDirPath = path.join(metaDir, subDir);
|
|
427
|
+
const stat = await fs.stat(subDirPath);
|
|
428
|
+
if (!stat.isDirectory()) continue;
|
|
429
|
+
|
|
430
|
+
const files = await fs.readdir(subDirPath);
|
|
431
|
+
|
|
432
|
+
for (const file of files) {
|
|
433
|
+
if (entries.length >= limit) break;
|
|
434
|
+
if (!file.endsWith('.json')) continue;
|
|
435
|
+
|
|
436
|
+
const metaPath = path.join(subDirPath, file);
|
|
437
|
+
try {
|
|
438
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
439
|
+
const meta: CacheEntryMeta = JSON.parse(metaContent);
|
|
440
|
+
|
|
441
|
+
// 期限切れフィルタ
|
|
442
|
+
if (!options.includeExpired && new Date(meta.expiresAt) < new Date()) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ソースフィルタ
|
|
447
|
+
if (options.source && meta.source !== options.source) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// タグフィルタ
|
|
452
|
+
if (options.tags && options.tags.length > 0) {
|
|
453
|
+
const metaTags = meta.tags || [];
|
|
454
|
+
const hasAllTags = options.tags.every((tag) => metaTags.includes(tag));
|
|
455
|
+
if (!hasAllTags) continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// データを読み込み
|
|
459
|
+
const hash = file.replace('.json', '');
|
|
460
|
+
const dataPath = path.join(this.config.cacheDir, 'data', subDir, `${hash}.json`);
|
|
461
|
+
const dataContent = await fs.readFile(dataPath, 'utf-8');
|
|
462
|
+
const value = JSON.parse(dataContent);
|
|
463
|
+
|
|
464
|
+
entries.push({
|
|
465
|
+
key: meta.originalKey || `${meta.source}:${meta.queryHash}`,
|
|
466
|
+
value,
|
|
467
|
+
meta,
|
|
468
|
+
});
|
|
469
|
+
} catch {
|
|
470
|
+
// ファイル読み込みエラーは無視
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
// ディレクトリ読み込みエラーは無視
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return entries;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* ヒット率を更新
|
|
483
|
+
*/
|
|
484
|
+
private updateHitRate(): void {
|
|
485
|
+
const total = this.stats.hits + this.stats.misses;
|
|
486
|
+
this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* ソース別統計を更新
|
|
491
|
+
*/
|
|
492
|
+
private updateSourceStats(
|
|
493
|
+
source: CacheSource,
|
|
494
|
+
action: 'hit' | 'miss' | 'add' | 'remove',
|
|
495
|
+
size?: number
|
|
496
|
+
): void {
|
|
497
|
+
if (!this.stats.bySource[source]) {
|
|
498
|
+
this.stats.bySource[source] = {
|
|
499
|
+
entries: 0,
|
|
500
|
+
sizeBytes: 0,
|
|
501
|
+
hits: 0,
|
|
502
|
+
misses: 0,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const sourceStats = this.stats.bySource[source] as SourceCacheStats;
|
|
507
|
+
|
|
508
|
+
switch (action) {
|
|
509
|
+
case 'hit':
|
|
510
|
+
sourceStats.hits++;
|
|
511
|
+
break;
|
|
512
|
+
case 'miss':
|
|
513
|
+
sourceStats.misses++;
|
|
514
|
+
break;
|
|
515
|
+
case 'add':
|
|
516
|
+
sourceStats.entries++;
|
|
517
|
+
sourceStats.sizeBytes += size || 0;
|
|
518
|
+
break;
|
|
519
|
+
case 'remove':
|
|
520
|
+
sourceStats.entries = Math.max(0, sourceStats.entries - 1);
|
|
521
|
+
sourceStats.sizeBytes = Math.max(0, sourceStats.sizeBytes - (size || 0));
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 統計情報を読み込み
|
|
528
|
+
*/
|
|
529
|
+
private async loadStats(): Promise<void> {
|
|
530
|
+
const statsPath = path.join(this.config.cacheDir, 'stats.json');
|
|
531
|
+
try {
|
|
532
|
+
const content = await fs.readFile(statsPath, 'utf-8');
|
|
533
|
+
const savedStats = JSON.parse(content);
|
|
534
|
+
this.stats = { ...this.stats, ...savedStats };
|
|
535
|
+
} catch {
|
|
536
|
+
// ファイルがない場合は初期値を使用
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 統計情報を保存
|
|
542
|
+
*/
|
|
543
|
+
async saveStats(): Promise<void> {
|
|
544
|
+
await this.ensureInitialized();
|
|
545
|
+
const statsPath = path.join(this.config.cacheDir, 'stats.json');
|
|
546
|
+
await fs.writeFile(statsPath, JSON.stringify(this.stats, null, 2));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* デフォルトのキャッシュストアインスタンスを作成
|
|
552
|
+
*/
|
|
553
|
+
export function createCacheStore(config?: Partial<CacheConfig>): FileCacheStore {
|
|
554
|
+
return new FileCacheStore(config);
|
|
555
|
+
}
|