Skip to content

Commit d16de1e

Browse files
authored
chore/refactor-duplicate-code (#16)
* refactor: consolidate duplicate caching and utility code - Create unified CacheManager to handle in-memory and disk caching - Extract ConsoleUtils for progress display operations - Add CursorUtils.cleanup() for terminal state restoration - Move stripAnsi to VersionUtils for shared ANSI code handling - Remove duplicate caching logic from jsdelivr-registry and npm-registry * feat(ui): improve filter mode behavior and persistence Allow users to edit existing filters by pressing '/' again, and display the active filter even when not in filter mode. Escape key now clears the filter instead of exiting the CLI, providing a more intuitive search experience. * refactor(ui): update package list status line display * refactor(ui): update filter mode status line display * refactor(ui): remove warning when no packages selected * style: format code
1 parent fdffab7 commit d16de1e

15 files changed

Lines changed: 305 additions & 192 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,5 @@ ehthumbs.db
8989
Thumbs.db
9090

9191
dist/
92+
93+
.claude

src/core/package-detector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../utils'
1010
import { getAllPackageDataFromJsdelivr, getAllPackageData } from '../services'
1111
import { DEFAULT_REGISTRY, isPackageIgnored } from '../config'
12+
import { ConsoleUtils } from '../ui/utils'
1213

1314
export class PackageDetector {
1415
private packageJsonPath: string | null = null
@@ -198,8 +199,7 @@ export class PackageDetector {
198199
}
199200

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

205205
public getOutdatedPackagesOnly(packages: PackageInfo[]): PackageInfo[] {

src/interactive-ui.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ export class InteractiveUI {
237237
}
238238
break
239239
case 'enter_filter_mode':
240-
stateManager.enterFilterMode()
240+
stateManager.enterFilterMode(action.preserveQuery)
241241
break
242242
case 'exit_filter_mode':
243-
stateManager.exitFilterMode()
243+
stateManager.exitFilterMode(action.clearQuery)
244244
break
245245
case 'filter_input':
246246
stateManager.appendToFilterQuery(action.char)

src/services/cache-manager.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { CACHE_TTL } from '../config'
2+
import { persistentCache } from './persistent-cache'
3+
4+
/**
5+
* Package version data structure
6+
*/
7+
export interface PackageVersionData {
8+
latestVersion: string
9+
allVersions: string[]
10+
}
11+
12+
/**
13+
* In-memory cache entry with timestamp for TTL
14+
*/
15+
interface CacheEntry<T> {
16+
data: T
17+
timestamp: number
18+
}
19+
20+
/**
21+
* Unified cache manager that handles both in-memory and persistent disk caching.
22+
* Consolidates caching logic used across registry services.
23+
*/
24+
export class CacheManager<T = PackageVersionData> {
25+
private memoryCache = new Map<string, CacheEntry<T>>()
26+
private ttl: number
27+
28+
constructor(ttl: number = CACHE_TTL) {
29+
this.ttl = ttl
30+
}
31+
32+
/**
33+
* Get cached data for a key, checking memory first, then disk.
34+
* Returns null if not found or expired.
35+
*/
36+
get(key: string): T | null {
37+
// Check in-memory cache first (fastest)
38+
const memoryCached = this.memoryCache.get(key)
39+
if (memoryCached && Date.now() - memoryCached.timestamp < this.ttl) {
40+
return memoryCached.data
41+
}
42+
43+
// Check persistent disk cache (survives restarts)
44+
const diskCached = persistentCache.get(key)
45+
if (diskCached) {
46+
// Populate in-memory cache for subsequent accesses
47+
this.memoryCache.set(key, {
48+
data: diskCached as T,
49+
timestamp: Date.now(),
50+
})
51+
return diskCached as T
52+
}
53+
54+
return null
55+
}
56+
57+
/**
58+
* Store data in both memory and disk cache.
59+
*/
60+
set(key: string, data: T): void {
61+
// Cache in memory
62+
this.memoryCache.set(key, {
63+
data,
64+
timestamp: Date.now(),
65+
})
66+
67+
// Cache to disk for persistence
68+
persistentCache.set(key, data as PackageVersionData)
69+
}
70+
71+
/**
72+
* Get data from cache or fetch it using the provided fetcher function.
73+
* This is the main entry point for cache-aside pattern.
74+
*/
75+
async getOrFetch(key: string, fetcher: () => Promise<T | null>): Promise<T | null> {
76+
// Try cache first
77+
const cached = this.get(key)
78+
if (cached) {
79+
return cached
80+
}
81+
82+
// Fetch fresh data
83+
const data = await fetcher()
84+
if (data) {
85+
this.set(key, data)
86+
}
87+
88+
return data
89+
}
90+
91+
/**
92+
* Check if a key exists and is not expired in cache.
93+
*/
94+
has(key: string): boolean {
95+
return this.get(key) !== null
96+
}
97+
98+
/**
99+
* Clear in-memory cache (useful for testing).
100+
*/
101+
clear(): void {
102+
this.memoryCache.clear()
103+
}
104+
105+
/**
106+
* Flush pending disk cache writes.
107+
*/
108+
flush(): void {
109+
persistentCache.flush()
110+
}
111+
112+
/**
113+
* Get cache statistics.
114+
*/
115+
getStats(): { memoryEntries: number; diskStats: { entries: number; cacheDir: string } } {
116+
return {
117+
memoryEntries: this.memoryCache.size,
118+
diskStats: persistentCache.getStats(),
119+
}
120+
}
121+
}
122+
123+
// Default package version cache instance
124+
export const packageCache = new CacheManager<PackageVersionData>()

src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './jsdelivr-registry'
77
export * from './changelog-fetcher'
88
export * from './version-checker'
99
export * from './persistent-cache'
10+
export * from './cache-manager'

src/services/jsdelivr-registry.ts

Lines changed: 22 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Pool, request } from 'undici'
22
import * as semver from 'semver'
3-
import { CACHE_TTL, JSDELIVR_CDN_URL, MAX_CONCURRENT_REQUESTS, REQUEST_TIMEOUT } from '../config'
3+
import { JSDELIVR_CDN_URL, MAX_CONCURRENT_REQUESTS, REQUEST_TIMEOUT } from '../config'
44
import { getAllPackageData } from './npm-registry'
5-
import { persistentCache } from './persistent-cache'
5+
import { packageCache, PackageVersionData } from './cache-manager'
6+
import { ConsoleUtils } from '../ui/utils'
67
import { OnBatchReadyCallback } from '../types'
78

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

22-
// In-memory cache for package data
23-
interface CacheEntry {
24-
data: { latestVersion: string; allVersions: string[] }
25-
timestamp: number
26-
}
27-
const packageCache = new Map<string, CacheEntry>()
28-
2923
/**
3024
* Fetches package.json from jsdelivr CDN for a specific version tag using undici pool.
3125
* Uses connection pooling and keep-alive for maximum performance.
@@ -81,8 +75,8 @@ export async function getAllPackageDataFromJsdelivr(
8175
currentVersions?: Map<string, string>,
8276
onProgress?: (currentPackage: string, completed: number, total: number) => void,
8377
onBatchReady?: OnBatchReadyCallback
84-
): Promise<Map<string, { latestVersion: string; allVersions: string[] }>> {
85-
const packageData = new Map<string, { latestVersion: string; allVersions: string[] }>()
78+
): Promise<Map<string, PackageVersionData>> {
79+
const packageData = new Map<string, PackageVersionData>()
8680

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

9488
// Batch buffer for progressive updates
95-
let batchBuffer: Array<{ name: string; data: { latestVersion: string; allVersions: string[] } }> =
96-
[]
89+
let batchBuffer: Array<{ name: string; data: PackageVersionData }> = []
9790
let batchTimer: NodeJS.Timeout | null = null
9891

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

111104
// Helper to add package to batch and flush if needed
112-
const addToBatch = (
113-
packageName: string,
114-
data: { latestVersion: string; allVersions: string[] }
115-
) => {
105+
const addToBatch = (packageName: string, data: PackageVersionData) => {
116106
if (onBatchReady) {
117107
batchBuffer.push({ name: packageName, data })
118108

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

133-
// Try to get from in-memory cache first (fastest)
134-
const memoryCached = packageCache.get(packageName)
135-
if (memoryCached && Date.now() - memoryCached.timestamp < CACHE_TTL) {
136-
packageData.set(packageName, memoryCached.data)
137-
completedCount++
138-
if (onProgress) {
139-
onProgress(packageName, completedCount, total)
140-
}
141-
addToBatch(packageName, memoryCached.data)
142-
return
143-
}
144-
145-
// Try persistent disk cache (fast, survives restarts)
146-
const diskCached = persistentCache.get(packageName)
147-
if (diskCached) {
148-
// Also populate in-memory cache for subsequent accesses
149-
packageCache.set(packageName, {
150-
data: diskCached,
151-
timestamp: Date.now(),
152-
})
153-
packageData.set(packageName, diskCached)
123+
// Use CacheManager for unified caching (memory + disk)
124+
const cached = packageCache.get(packageName)
125+
if (cached) {
126+
packageData.set(packageName, cached)
154127
completedCount++
155128
if (onProgress) {
156129
onProgress(packageName, completedCount, total)
157130
}
158-
addToBatch(packageName, diskCached)
131+
addToBatch(packageName, cached)
159132
return
160133
}
161134

@@ -187,13 +160,8 @@ export async function getAllPackageDataFromJsdelivr(
187160

188161
if (result) {
189162
packageData.set(packageName, result)
190-
// Cache in memory
191-
packageCache.set(packageName, {
192-
data: result,
193-
timestamp: Date.now(),
194-
})
195-
// Cache to disk for persistence
196-
persistentCache.set(packageName, result)
163+
// CacheManager handles both memory and disk caching
164+
packageCache.set(packageName, result)
197165
addToBatch(packageName, result)
198166
}
199167

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

215-
const result = {
183+
const result: PackageVersionData = {
216184
latestVersion,
217185
allVersions: allVersions.sort(semver.rcompare),
218186
}
219187

220-
// Cache the result in memory
221-
packageCache.set(packageName, {
222-
data: result,
223-
timestamp: Date.now(),
224-
})
225-
// Cache to disk for persistence
226-
persistentCache.set(packageName, result)
188+
// Cache the result using CacheManager (handles both memory and disk)
189+
packageCache.set(packageName, result)
227190

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

241204
if (result) {
242205
packageData.set(packageName, result)
243-
// Cache in memory
244-
packageCache.set(packageName, {
245-
data: result,
246-
timestamp: Date.now(),
247-
})
248-
// Cache to disk for persistence
249-
persistentCache.set(packageName, result)
206+
// CacheManager handles both memory and disk caching
207+
packageCache.set(packageName, result)
250208
addToBatch(packageName, result)
251209
}
252210
} catch (npmError) {
@@ -267,11 +225,11 @@ export async function getAllPackageDataFromJsdelivr(
267225
flushBatch()
268226

269227
// Flush persistent cache to disk
270-
persistentCache.flush()
228+
packageCache.flush()
271229

272-
// Clear the progress line and show completion time if no custom progress handler
230+
// Clear the progress line if no custom progress handler
273231
if (!onProgress) {
274-
process.stdout.write('\r' + ' '.repeat(80) + '\r')
232+
ConsoleUtils.clearProgress()
275233
}
276234

277235
return packageData

0 commit comments

Comments
 (0)