aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
332 lines (282 loc) • 10.2 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { createHash } from 'crypto';
/**
* GitHub API 캐싱 시스템
* - API 호출 결과를 메모리와 디스크에 캐싱
* - TTL 기반 캐시 만료 처리
* - 중복 요청 방지
*/
class GitHubAPICache {
constructor(cacheDir = '.aiwf/cache', ttl = 300000) { // 5분 TTL
this.cacheDir = cacheDir;
this.ttl = ttl;
this.memoryCache = new Map();
this.pendingRequests = new Map();
this.stats = {
hits: 0,
misses: 0,
requests: 0
};
}
async init() {
try {
await fs.mkdir(this.cacheDir, { recursive: true });
} catch (error) {
console.warn('Failed to create cache directory:', error.message);
}
}
// 캐시 키 생성
generateCacheKey(url, headers = {}) {
const keyData = JSON.stringify({ url, headers });
return createHash('sha256').update(keyData).digest('hex');
}
// 메모리 캐시에서 가져오기
getFromMemory(key) {
const cached = this.memoryCache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
this.stats.hits++;
return cached.data;
}
return null;
}
// 디스크 캐시에서 가져오기
async getFromDisk(key) {
try {
const cachePath = path.join(this.cacheDir, `${key}.json`);
const cacheData = await fs.readFile(cachePath, 'utf8');
const cached = JSON.parse(cacheData);
if (Date.now() - cached.timestamp < this.ttl) {
// 메모리 캐시에도 저장
this.memoryCache.set(key, cached);
this.stats.hits++;
return cached.data;
} else {
// 만료된 캐시 삭제
await fs.unlink(cachePath).catch(() => {});
}
} catch (error) {
// 캐시 파일이 없거나 읽기 실패
}
return null;
}
// 캐시에 저장
async saveToCache(key, data) {
const cacheEntry = {
data,
timestamp: Date.now()
};
// 메모리 캐시에 저장
this.memoryCache.set(key, cacheEntry);
// 디스크 캐시에 저장
try {
const cachePath = path.join(this.cacheDir, `${key}.json`);
await fs.writeFile(cachePath, JSON.stringify(cacheEntry, null, 2));
} catch (error) {
console.warn('Failed to save to disk cache:', error.message);
}
}
// 캐시된 데이터 가져오기
async get(url, headers = {}) {
const key = this.generateCacheKey(url, headers);
// 메모리 캐시 확인
let cachedData = this.getFromMemory(key);
if (cachedData) {
return cachedData;
}
// 디스크 캐시 확인
cachedData = await this.getFromDisk(key);
if (cachedData) {
return cachedData;
}
this.stats.misses++;
return null;
}
// 캐시에 저장
async set(url, data, headers = {}) {
const key = this.generateCacheKey(url, headers);
await this.saveToCache(key, data);
}
// 중복 요청 방지
async getOrFetch(url, fetchFunction, headers = {}) {
const key = this.generateCacheKey(url, headers);
this.stats.requests++;
// 캐시에서 확인
const cachedData = await this.get(url, headers);
if (cachedData) {
return cachedData;
}
// 이미 진행 중인 요청이 있는지 확인
if (this.pendingRequests.has(key)) {
return await this.pendingRequests.get(key);
}
// 새로운 요청 시작
const fetchPromise = fetchFunction().then(async (data) => {
await this.set(url, data, headers);
this.pendingRequests.delete(key);
return data;
}).catch((error) => {
this.pendingRequests.delete(key);
throw error;
});
this.pendingRequests.set(key, fetchPromise);
return await fetchPromise;
}
// 캐시 통계
getStats() {
return {
...this.stats,
hitRate: this.stats.requests > 0 ? (this.stats.hits / this.stats.requests * 100).toFixed(2) + '%' : '0%',
memorySize: this.memoryCache.size,
cacheDir: this.cacheDir
};
}
// 캐시 정리
async cleanup() {
const now = Date.now();
const expiredKeys = [];
// 메모리 캐시 정리
for (const [key, cached] of this.memoryCache.entries()) {
if (now - cached.timestamp >= this.ttl) {
expiredKeys.push(key);
}
}
expiredKeys.forEach(key => this.memoryCache.delete(key));
// 디스크 캐시 정리
try {
const files = await fs.readdir(this.cacheDir);
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(this.cacheDir, file);
try {
const stat = await fs.stat(filePath);
if (now - stat.mtime.getTime() >= this.ttl) {
await fs.unlink(filePath);
}
} catch (error) {
// 파일 삭제 실패는 무시
}
}
}
} catch (error) {
// 디렉토리 읽기 실패는 무시
}
return {
expiredMemoryKeys: expiredKeys.length,
message: `Cleaned up ${expiredKeys.length} expired cache entries`
};
}
// 전체 캐시 초기화
async clear() {
this.memoryCache.clear();
this.stats = { hits: 0, misses: 0, requests: 0 };
try {
const files = await fs.readdir(this.cacheDir);
for (const file of files) {
if (file.endsWith('.json')) {
await fs.unlink(path.join(this.cacheDir, file));
}
}
} catch (error) {
// 디렉토리 읽기 실패는 무시
}
}
}
// 배치 처리를 위한 GitHub API 클라이언트
class OptimizedGitHubClient {
constructor(baseUrl = 'https://api.github.com', cacheOptions = {}) {
this.baseUrl = baseUrl;
this.cache = new GitHubAPICache(cacheOptions.cacheDir, cacheOptions.ttl);
this.rateLimiter = new RateLimiter(60, 60000); // 60 requests per minute
this.batchQueue = [];
this.batchTimeout = null;
}
async init() {
await this.cache.init();
}
// 캐시된 HTTP 요청
async cachedFetch(url, options = {}) {
const headers = { 'User-Agent': 'aiwf', ...options.headers };
return await this.cache.getOrFetch(url, async () => {
// Rate limiting
await this.rateLimiter.waitForNext();
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}, headers);
}
// 배치 요청 처리
async batchRequest(urls, options = {}) {
const results = [];
const batchSize = options.batchSize || 5;
for (let i = 0; i < urls.length; i += batchSize) {
const batch = urls.slice(i, i + batchSize);
const batchPromises = batch.map(url => this.cachedFetch(url, options));
try {
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
} catch (error) {
console.error('Batch request failed:', error);
results.push(...batch.map(() => ({ status: 'rejected', reason: error })));
}
}
return results;
}
// 디렉토리 구조 가져오기 (캐시 적용)
async getDirectoryStructure(repoPath, dirPath = '') {
const url = `${this.baseUrl}/repos/${repoPath}/contents/${dirPath}`;
return await this.cachedFetch(url);
}
// 파일 내용 가져오기 (캐시 적용)
async getFileContent(repoPath, filePath) {
const url = `${this.baseUrl}/repos/${repoPath}/contents/${filePath}`;
return await this.cachedFetch(url);
}
// 통계 정보
getStats() {
return {
cache: this.cache.getStats(),
rateLimit: this.rateLimiter.getStats()
};
}
// 캐시 정리
async cleanup() {
return await this.cache.cleanup();
}
}
// Rate Limiter 클래스
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
this.stats = { totalRequests: 0, throttledRequests: 0 };
}
async waitForNext() {
const now = Date.now();
// 시간 윈도우를 벗어난 요청 제거
this.requests = this.requests.filter(time => now - time < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = Math.min(...this.requests);
const waitTime = this.timeWindow - (now - oldestRequest);
if (waitTime > 0) {
this.stats.throttledRequests++;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
this.requests.push(now);
this.stats.totalRequests++;
}
getStats() {
return {
...this.stats,
currentRequests: this.requests.length,
maxRequests: this.maxRequests,
timeWindow: this.timeWindow
};
}
}
export { GitHubAPICache, OptimizedGitHubClient, RateLimiter };