diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index ddaa90f1e2b..4a639c97a3e 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -5,6 +5,7 @@ import { State } from "./state" import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { Filesystem } from "@/util/filesystem" +import { createLruCache } from "@/util/cache" interface Context { directory: string @@ -12,7 +13,9 @@ interface Context { project: Project.Info } const context = Context.create("instance") -const cache = new Map>() +const cache = createLruCache>({ + maxEntries: 20, +}) export const Instance = { async provide(input: { directory: string; init?: () => Promise; fn: () => R }): Promise { diff --git a/packages/opencode/src/util/cache.ts b/packages/opencode/src/util/cache.ts new file mode 100644 index 00000000000..107e72172b6 --- /dev/null +++ b/packages/opencode/src/util/cache.ts @@ -0,0 +1,86 @@ +/** + * LRU cache with max entries limit for preventing memory leaks + */ + +export type LruCacheOpts = { + maxEntries?: number + onEvict?: (key: any, value: any) => void +} + +type LruCacheEntry = { + value: V + lastAccess: number +} + +export function createLruCache(opts: LruCacheOpts = {}) { + const { maxEntries = Infinity, onEvict } = opts + const cache = new Map>() + + function evictOne() { + let oldestKey: K | null = null + let oldestAccess = Infinity + + for (const [key, entry] of cache) { + if (entry.lastAccess < oldestAccess) { + oldestAccess = entry.lastAccess + oldestKey = key + } + } + + if (oldestKey !== null) { + delete_(oldestKey) + } + } + + function delete_(key: K): boolean { + const entry = cache.get(key) + if (!entry) return false + onEvict?.(key, entry.value) + return cache.delete(key) + } + + return { + get(key: K): V | undefined { + const entry = cache.get(key) + if (!entry) return undefined + entry.lastAccess = Date.now() + return entry.value + }, + + set(key: K, value: V): void { + if (cache.size >= maxEntries && !cache.has(key)) { + evictOne() + } + cache.set(key, { value, lastAccess: Date.now() }) + }, + + has(key: K): boolean { + return cache.has(key) + }, + + delete(key: K): boolean { + return delete_(key) + }, + + clear(): void { + for (const [key, entry] of cache) { + onEvict?.(key, entry.value) + } + cache.clear() + }, + + get size() { + return cache.size + }, + + *[Symbol.iterator](): IterableIterator<[K, V]> { + for (const [key, entry] of cache) { + yield [key, entry.value] + } + }, + + entries(): IterableIterator<[K, V]> { + return this[Symbol.iterator]() + }, + } +}