Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ ehthumbs.db
Thumbs.db

dist/

.claude
4 changes: 2 additions & 2 deletions src/core/package-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../utils'
import { getAllPackageDataFromJsdelivr, getAllPackageData } from '../services'
import { DEFAULT_REGISTRY, isPackageIgnored } from '../config'
import { ConsoleUtils } from '../ui/utils'

export class PackageDetector {
private packageJsonPath: string | null = null
Expand Down Expand Up @@ -198,8 +199,7 @@ export class PackageDetector {
}

private showProgress(message: string): void {
// Clear current line and show new message
process.stdout.write(`\r${' '.repeat(80)}\r${message}`)
ConsoleUtils.showProgress(message)
}

public getOutdatedPackagesOnly(packages: PackageInfo[]): PackageInfo[] {
Expand Down
4 changes: 2 additions & 2 deletions src/interactive-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ export class InteractiveUI {
}
break
case 'enter_filter_mode':
stateManager.enterFilterMode()
stateManager.enterFilterMode(action.preserveQuery)
break
case 'exit_filter_mode':
stateManager.exitFilterMode()
stateManager.exitFilterMode(action.clearQuery)
break
case 'filter_input':
stateManager.appendToFilterQuery(action.char)
Expand Down
124 changes: 124 additions & 0 deletions src/services/cache-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { CACHE_TTL } from '../config'
import { persistentCache } from './persistent-cache'

/**
* Package version data structure
*/
export interface PackageVersionData {
latestVersion: string
allVersions: string[]
}

/**
* In-memory cache entry with timestamp for TTL
*/
interface CacheEntry<T> {
data: T
timestamp: number
}

/**
* Unified cache manager that handles both in-memory and persistent disk caching.
* Consolidates caching logic used across registry services.
*/
export class CacheManager<T = PackageVersionData> {
private memoryCache = new Map<string, CacheEntry<T>>()
private ttl: number

constructor(ttl: number = CACHE_TTL) {
this.ttl = ttl
}

/**
* Get cached data for a key, checking memory first, then disk.
* Returns null if not found or expired.
*/
get(key: string): T | null {
// Check in-memory cache first (fastest)
const memoryCached = this.memoryCache.get(key)
if (memoryCached && Date.now() - memoryCached.timestamp < this.ttl) {
return memoryCached.data
}

// Check persistent disk cache (survives restarts)
const diskCached = persistentCache.get(key)
if (diskCached) {
// Populate in-memory cache for subsequent accesses
this.memoryCache.set(key, {
data: diskCached as T,
timestamp: Date.now(),
})
return diskCached as T
}

return null
}

/**
* Store data in both memory and disk cache.
*/
set(key: string, data: T): void {
// Cache in memory
this.memoryCache.set(key, {
data,
timestamp: Date.now(),
})

// Cache to disk for persistence
persistentCache.set(key, data as PackageVersionData)
}

/**
* Get data from cache or fetch it using the provided fetcher function.
* This is the main entry point for cache-aside pattern.
*/
async getOrFetch(key: string, fetcher: () => Promise<T | null>): Promise<T | null> {
// Try cache first
const cached = this.get(key)
if (cached) {
return cached
}

// Fetch fresh data
const data = await fetcher()
if (data) {
this.set(key, data)
}

return data
}

/**
* Check if a key exists and is not expired in cache.
*/
has(key: string): boolean {
return this.get(key) !== null
}

/**
* Clear in-memory cache (useful for testing).
*/
clear(): void {
this.memoryCache.clear()
}

/**
* Flush pending disk cache writes.
*/
flush(): void {
persistentCache.flush()
}

/**
* Get cache statistics.
*/
getStats(): { memoryEntries: number; diskStats: { entries: number; cacheDir: string } } {
return {
memoryEntries: this.memoryCache.size,
diskStats: persistentCache.getStats(),
}
}
}

// Default package version cache instance
export const packageCache = new CacheManager<PackageVersionData>()
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './jsdelivr-registry'
export * from './changelog-fetcher'
export * from './version-checker'
export * from './persistent-cache'
export * from './cache-manager'
86 changes: 22 additions & 64 deletions src/services/jsdelivr-registry.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Pool, request } from 'undici'
import * as semver from 'semver'
import { CACHE_TTL, JSDELIVR_CDN_URL, MAX_CONCURRENT_REQUESTS, REQUEST_TIMEOUT } from '../config'
import { JSDELIVR_CDN_URL, MAX_CONCURRENT_REQUESTS, REQUEST_TIMEOUT } from '../config'
import { getAllPackageData } from './npm-registry'
import { persistentCache } from './persistent-cache'
import { packageCache, PackageVersionData } from './cache-manager'
import { ConsoleUtils } from '../ui/utils'
import { OnBatchReadyCallback } from '../types'

// Create a persistent connection pool for jsDelivr CDN with optimal settings
Expand All @@ -19,13 +20,6 @@ const jsdelivrPool = new Pool('https://cdn.jsdelivr.net', {
const BATCH_SIZE = 5
const BATCH_TIMEOUT_MS = 500

// In-memory cache for package data
interface CacheEntry {
data: { latestVersion: string; allVersions: string[] }
timestamp: number
}
const packageCache = new Map<string, CacheEntry>()

/**
* Fetches package.json from jsdelivr CDN for a specific version tag using undici pool.
* Uses connection pooling and keep-alive for maximum performance.
Expand Down Expand Up @@ -81,8 +75,8 @@ export async function getAllPackageDataFromJsdelivr(
currentVersions?: Map<string, string>,
onProgress?: (currentPackage: string, completed: number, total: number) => void,
onBatchReady?: OnBatchReadyCallback
): Promise<Map<string, { latestVersion: string; allVersions: string[] }>> {
const packageData = new Map<string, { latestVersion: string; allVersions: string[] }>()
): Promise<Map<string, PackageVersionData>> {
const packageData = new Map<string, PackageVersionData>()

if (packageNames.length === 0) {
return packageData
Expand All @@ -92,8 +86,7 @@ export async function getAllPackageDataFromJsdelivr(
let completedCount = 0

// Batch buffer for progressive updates
let batchBuffer: Array<{ name: string; data: { latestVersion: string; allVersions: string[] } }> =
[]
let batchBuffer: Array<{ name: string; data: PackageVersionData }> = []
let batchTimer: NodeJS.Timeout | null = null

// Helper to flush the current batch
Expand All @@ -109,10 +102,7 @@ export async function getAllPackageDataFromJsdelivr(
}

// Helper to add package to batch and flush if needed
const addToBatch = (
packageName: string,
data: { latestVersion: string; allVersions: string[] }
) => {
const addToBatch = (packageName: string, data: PackageVersionData) => {
if (onBatchReady) {
batchBuffer.push({ name: packageName, data })

Expand All @@ -130,32 +120,15 @@ export async function getAllPackageDataFromJsdelivr(
const fetchPackageWithFallback = async (packageName: string): Promise<void> => {
const currentVersion = currentVersions?.get(packageName)

// Try to get from in-memory cache first (fastest)
const memoryCached = packageCache.get(packageName)
if (memoryCached && Date.now() - memoryCached.timestamp < CACHE_TTL) {
packageData.set(packageName, memoryCached.data)
completedCount++
if (onProgress) {
onProgress(packageName, completedCount, total)
}
addToBatch(packageName, memoryCached.data)
return
}

// Try persistent disk cache (fast, survives restarts)
const diskCached = persistentCache.get(packageName)
if (diskCached) {
// Also populate in-memory cache for subsequent accesses
packageCache.set(packageName, {
data: diskCached,
timestamp: Date.now(),
})
packageData.set(packageName, diskCached)
// Use CacheManager for unified caching (memory + disk)
const cached = packageCache.get(packageName)
if (cached) {
packageData.set(packageName, cached)
completedCount++
if (onProgress) {
onProgress(packageName, completedCount, total)
}
addToBatch(packageName, diskCached)
addToBatch(packageName, cached)
return
}

Expand Down Expand Up @@ -187,13 +160,8 @@ export async function getAllPackageDataFromJsdelivr(

if (result) {
packageData.set(packageName, result)
// Cache in memory
packageCache.set(packageName, {
data: result,
timestamp: Date.now(),
})
// Cache to disk for persistence
persistentCache.set(packageName, result)
// CacheManager handles both memory and disk caching
packageCache.set(packageName, result)
addToBatch(packageName, result)
}

Expand All @@ -212,18 +180,13 @@ export async function getAllPackageDataFromJsdelivr(
allVersions.push(majorResult.version)
}

const result = {
const result: PackageVersionData = {
latestVersion,
allVersions: allVersions.sort(semver.rcompare),
}

// Cache the result in memory
packageCache.set(packageName, {
data: result,
timestamp: Date.now(),
})
// Cache to disk for persistence
persistentCache.set(packageName, result)
// Cache the result using CacheManager (handles both memory and disk)
packageCache.set(packageName, result)

packageData.set(packageName, result)
completedCount++
Expand All @@ -240,13 +203,8 @@ export async function getAllPackageDataFromJsdelivr(

if (result) {
packageData.set(packageName, result)
// Cache in memory
packageCache.set(packageName, {
data: result,
timestamp: Date.now(),
})
// Cache to disk for persistence
persistentCache.set(packageName, result)
// CacheManager handles both memory and disk caching
packageCache.set(packageName, result)
addToBatch(packageName, result)
}
} catch (npmError) {
Expand All @@ -267,11 +225,11 @@ export async function getAllPackageDataFromJsdelivr(
flushBatch()

// Flush persistent cache to disk
persistentCache.flush()
packageCache.flush()

// Clear the progress line and show completion time if no custom progress handler
// Clear the progress line if no custom progress handler
if (!onProgress) {
process.stdout.write('\r' + ' '.repeat(80) + '\r')
ConsoleUtils.clearProgress()
}

return packageData
Expand Down
Loading
Loading