mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
109 lines
3.8 KiB
Markdown
109 lines
3.8 KiB
Markdown
## Persist in-memory cache bounds
|
|
|
|
Fix unbounded `persist.ts` string cache growth
|
|
|
|
---
|
|
|
|
### Summary
|
|
|
|
`packages/app/src/utils/persist.ts` maintains a module-level `cache: Map<string, string>()` that mirrors values written to storage. This cache can retain very large JSON strings (prompt history, image dataUrls, terminal buffers) indefinitely, even after the underlying `localStorage` keys are evicted. Over long sessions this can become an in-process memory leak.
|
|
|
|
This spec adds explicit bounds (entries + approximate bytes) and makes eviction/removal paths delete from the in-memory cache.
|
|
|
|
---
|
|
|
|
### Scoped files (parallel-safe)
|
|
|
|
- `packages/app/src/utils/persist.ts`
|
|
|
|
---
|
|
|
|
### Goals
|
|
|
|
- Prevent unbounded memory growth from the module-level persist cache
|
|
- Ensure keys removed/evicted from storage are also removed from the in-memory cache
|
|
- Preserve current semantics when `localStorage` access throws (fallback mode)
|
|
- Keep changes self-contained to `persist.ts`
|
|
|
|
---
|
|
|
|
### Non-goals
|
|
|
|
- Changing persisted schemas or moving payloads out of KV storage (covered elsewhere)
|
|
- Introducing a shared cache utility used by multiple modules
|
|
|
|
---
|
|
|
|
### Current state
|
|
|
|
- `cache` stores raw strings for every key read/written.
|
|
- `evict()` removes items from `localStorage` but does not clear the corresponding entries from `cache`.
|
|
- When `localStorage` is unavailable (throws), reads fall back to `cache`, so the cache is also a functional in-memory persistence layer.
|
|
|
|
---
|
|
|
|
### Proposed approach
|
|
|
|
1. Replace `cache: Map<string, string>` with a bounded LRU-like map
|
|
|
|
- Store `{ value: string, bytes: number }` per key.
|
|
- Maintain a running `totalBytes`.
|
|
- Enforce caps:
|
|
- `CACHE_MAX_ENTRIES` (e.g. 500)
|
|
- `CACHE_MAX_BYTES` (e.g. 8 _ 1024 _ 1024)
|
|
- Use Map insertion order as LRU:
|
|
- On `get`, re-insert the key to the end.
|
|
- On `set`, insert/update then evict oldest until within bounds.
|
|
- Approximate bytes as `value.length * 2` (UTF-16) to avoid `TextEncoder` allocations.
|
|
|
|
2. Ensure all removal paths clear the in-memory cache
|
|
|
|
- In `localStorageDirect().removeItem` and `localStorageWithPrefix().removeItem`: already calls `cache.delete(name)`; keep.
|
|
- In `write(...)` failure recovery where it calls `storage.removeItem(key)`: also `cache.delete(key)`.
|
|
- In `evict(...)` loop where it removes large keys: also `cache.delete(item.key)`.
|
|
|
|
3. Add a small dev-only diagnostic (optional)
|
|
|
|
- In dev, expose a lightweight `cacheStats()` helper (entries, totalBytes) for debugging memory reports.
|
|
|
|
---
|
|
|
|
### Implementation steps
|
|
|
|
1. Introduce constants and cache helpers in `persist.ts`
|
|
|
|
- `const CACHE_MAX_ENTRIES = ...`
|
|
- `const CACHE_MAX_BYTES = ...`
|
|
- `function cacheSet(key: string, value: string)`
|
|
- `function cacheGet(key: string): string | undefined`
|
|
- `function cacheDelete(key: string)`
|
|
- `function cachePrune()`
|
|
|
|
2. Route all existing `cache.set/get/delete` calls through helpers
|
|
|
|
- `localStorageDirect()`
|
|
- `localStorageWithPrefix()`
|
|
|
|
3. Update `evict()` and `write()` to delete from cache when deleting from storage
|
|
|
|
4. (Optional) Add dev logging guardrails
|
|
|
|
- If a single value exceeds `CACHE_MAX_BYTES`, cache it but immediately prune older keys (or refuse to cache it and keep behavior consistent).
|
|
|
|
---
|
|
|
|
### Acceptance criteria
|
|
|
|
- Repeatedly loading/saving large persisted values does not cause unbounded `cache` growth (entries and bytes are capped)
|
|
- Values removed from `localStorage` by `evict()` are not returned later via the in-memory cache
|
|
- App remains functional when `localStorage` throws (fallback mode still returns cached values, subject to caps)
|
|
|
|
---
|
|
|
|
### Validation plan
|
|
|
|
- Manual:
|
|
- Open app, perform actions that write large state (image attachments, terminal output, long sessions)
|
|
- Use browser memory tools to confirm JS heap does not grow linearly with repeated writes
|
|
- Simulate quota eviction (force small storage quota / fill storage) and confirm `cache` does not retain evicted keys indefinitely
|