feat(persistence): add LRU draft cache with quota management#8518
Conversation
📝 WalkthroughWalkthroughAdds a V2 immutable draft cache (LRU semantics), a V2 storage I/O module for localStorage/sessionStorage, comprehensive Vitest tests for both, and a minor config/comment update adding a knip ignore tag. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant Cache as draftCacheV2
participant Storage as storageIO
participant LS as localStorage
App->>Storage: readIndex(workspaceId)
Storage->>LS: getItem(indexKey)
LS-->>Storage: JSON / null
Storage-->>App: DraftIndexV2 | null
App->>Cache: upsertEntry(index, path, meta, limit)
Cache->>Cache: hashPath(path)
Cache->>Cache: touchOrder(order, key)
Cache->>Cache: evictIfNeeded(limit)
Cache-->>App: { index, evicted }
App->>Storage: writeIndex(workspaceId, index)
Storage->>LS: setItem(indexKey, JSON.stringify(index))
LS-->>Storage: success / quota error
Storage-->>App: boolean
App->>Storage: deleteOrphanPayloads(workspaceId, indexKeys)
Storage->>LS: get all payload keys
Storage->>LS: removeItem(orphanKey) [for each orphan]
Storage-->>App: numberOfRemoved
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
🎨 Storybook Build Status✅ Build completed successfully! ⏰ Completed at: 02/20/2026, 05:58:01 AM UTC 🔗 Links🎉 Your Storybook is ready for review! |
|
Playwright: ✅ 516 passed, 0 failed · 5 flaky 📊 Browser Reports
|
86cf743 to
8212e12
Compare
436fa0b to
c2e62c7
Compare
8212e12 to
30b3c47
Compare
c2e62c7 to
c5de23a
Compare
30b3c47 to
186a3b9
Compare
c5de23a to
7c42cd4
Compare
7c42cd4 to
85a6545
Compare
ba3ec21 to
64fd51d
Compare
The base branch was changed.
📦 Bundle: 4.26 MB gzip ⚪ 0 BDetailsSummary
Category Glance App Entry Points — 21.4 kB (baseline 21.4 kB) • ⚪ 0 BMain entry bundles and manifests
Graph Workspace — 914 kB (baseline 914 kB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Views & Navigation — 68.6 kB (baseline 68.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Panels & Settings — 430 kB (baseline 430 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
User & Accounts — 16 kB (baseline 16 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Editors & Dialogs — 706 B (baseline 706 B) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
UI Components — 42.3 kB (baseline 42.3 kB) • ⚪ 0 BReusable component library chunks
Data & Services — 2.4 MB (baseline 2.4 MB) • ⚪ 0 BStores, services, APIs, and repositories
Utilities & Hooks — 57.6 kB (baseline 57.6 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Vendor & Third-Party — 8.69 MB (baseline 8.69 MB) • ⚪ 0 BExternal libraries and shared vendor chunks
Other — 7.38 MB (baseline 7.38 MB) • ⚪ 0 BBundles that do not match a named category
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workflow/persistence/base/draftCacheV2.ts`:
- Around line 108-143: moveEntry currently overwrites any existing entry at
newPath; add a collision guard that detects if entries[newKey] already exists
and surface that to callers instead of silently replacing it: inside moveEntry
(use symbols oldKey, newKey, entries, oldEntry) check const existing =
entries[newKey] before deleting oldKey; if existing and existing !== oldEntry
set a replacedKey variable (or return null if you prefer rejection) and include
replacedKey in the returned object so callers can evict/merge payloads
explicitly; ensure you still remove oldKey and update order (filter only oldKey,
avoid duplicating newKey) and set updatedAt as before.
In `@src/platform/workflow/persistence/base/storageIO.ts`:
- Around line 129-164: getPayloadKeys and deleteOrphanPayloads currently
enumerate localStorage without guarding against environments that block storage
access; wrap the enumeration in a defensive availability check and try/catch so
it returns an empty array (for getPayloadKeys) or zero deletions (for
deleteOrphanPayloads) instead of throwing. Concretely, in getPayloadKeys (and
the similar clearAllV2Storage routine) first check a storage-availability helper
if one exists (e.g., isStorageAvailable) or wrap the for-loop/localStorage calls
in try { ... } catch (e) { return []; } and in deleteOrphanPayloads handle the
case where getPayloadKeys returns an empty array (or propagate 0) so deletion is
skipped safely; ensure any calls to deletePayload are only invoked when storage
is accessible.
f3fe2a7 to
2ad08d6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workflow/persistence/base/draftCacheV2.test.ts`:
- Line 15: Replace string literal suite titles with function/identifier
references to satisfy vitest/prefer-describe-function-title: change
describe('draftCacheV2', ...) to describe(draftCacheV2, ...), and any inner
suites like describe('createEmptyIndex', ...) and describe('touchOrder', ...) to
describe(createEmptyIndex, ...) and describe(touchOrder, ...); ensure the
referenced symbols (draftCacheV2, createEmptyIndex, touchOrder, etc.) are
imported or available in the test file so the describe calls receive the actual
function/identifier instead of a string.
---
Duplicate comments:
In `@src/platform/workflow/persistence/base/draftCacheV2.ts`:
- Around line 108-143: moveEntry currently overwrites an existing entry when
newPath hashes to an existing key; add a collision guard at the top of
moveEntry: compute oldKey/newKey as now, and if newKey !== oldKey and
index.entries[newKey] exists, return null (or alternately throw) to prevent
silent overwrite; update callers (usages of moveEntry) to handle the null return
(run the suggested rg search to find call sites) and add tests for the collision
case.
In `@src/platform/workflow/persistence/base/storageIO.ts`:
- Around line 132-143: Guard storage enumeration in getPayloadKeys (and
similarly in deleteOrphanPayloads) by first checking storage availability and
wrapping any localStorage/sessionStorage access and loops in try/catch;
specifically, when iterating over localStorage keys using
StorageKeys.prefixes.draftPayload and localStorage.key(i), catch exceptions and
return an empty array (or no‑op for deletions) so the utilities degrade safely
in restricted environments instead of throwing.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/workflow/persistence/base/draftCacheV2.ts (1)
55-60: Prefer immutable eviction/removal (avoid in-place mutation).
order.shift()anddelete entries[...]mutate local state. To align with the immutability guideline, consider deriving trimmedorderandentriesvia non-mutating transforms (slice/filter/object rest) instead.As per coding guidelines: Avoid mutable state; prefer immutability and assignment at point of declaration.
Also applies to: 89-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/workflow/persistence/base/draftCacheV2.ts` around lines 55 - 60, The loop mutates local state using order.shift() and delete entries[oldest]; instead, create new immutable versions: compute a trimmedOrder (e.g., with slice or filter excluding evicted keys and draftKey) and a newEntries object built via object spread/selection that omits evicted keys rather than using delete; update the variables order and entries by reassigning these new immutable values (also apply the same approach for the removal at the later place that uses delete on entries), referencing the same variables order, entries, evicted, and draftKey in the eviction logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workflow/persistence/base/storageIO.ts`:
- Around line 58-63: The quota-detection in the catch blocks only checks for
error.name === 'QuotaExceededError', which misses Firefox's
NS_ERROR_DOM_QUOTA_REACHED (error.code === 1014); update the catch logic in both
writeIndex and writePayload to treat quota errors as false by checking for a
DOMException whose name is 'QuotaExceededError' OR 'NS_ERROR_DOM_QUOTA_REACHED'
and/or whose code equals 1014 (and include legacy numeric codes like 22 if
desired), then return false; otherwise rethrow the error.
---
Duplicate comments:
In `@src/platform/workflow/persistence/base/draftCacheV2.test.ts`:
- Around line 15-37: Update the test suites to use function/identifier
references instead of string literals: replace describe('createEmptyIndex', ...)
with describe(createEmptyIndex, ...) and replace describe('touchOrder', ...)
with describe(touchOrder, ...); keep the inner it blocks and expectations the
same so only the describe titles change to satisfy the
vitest/prefer-describe-function-title lint rule (referencing the
createEmptyIndex and touchOrder symbols).
---
Nitpick comments:
In `@src/platform/workflow/persistence/base/draftCacheV2.ts`:
- Around line 55-60: The loop mutates local state using order.shift() and delete
entries[oldest]; instead, create new immutable versions: compute a trimmedOrder
(e.g., with slice or filter excluding evicted keys and draftKey) and a
newEntries object built via object spread/selection that omits evicted keys
rather than using delete; update the variables order and entries by reassigning
these new immutable values (also apply the same approach for the removal at the
later place that uses delete on entries), referencing the same variables order,
entries, evicted, and draftKey in the eviction logic.
Add core V2 logic for draft management: - draftCacheV2: Pure functions for index manipulation (LRU ordering, entry management, orphan cleanup) - storageIO: localStorage/sessionStorage read/write with error handling, quota management, and workspace-scoped operations These modules are not yet wired to the app - they provide the foundation for the store rewrite in the next commit. Amp-Thread-ID: https://ampcode.com/threads/T-019c16f4-05a2-779d-aa0e-a0e098308a95 Co-authored-by: Amp <amp@ampcode.com>
b107ee5 to
4d6cd54
Compare
## Summary Adds an LRU (Least Recently Used) cache layer and storage I/O utilities that handle localStorage quota limits gracefully. When storage is full, the oldest drafts are automatically evicted to make room for new ones. ## Changes - **What**: - `draftCacheV2.ts` - In-memory LRU cache with configurable max entries (default 32) - `storageIO.ts` - Storage read/write with automatic quota management and eviction - **Why**: Users experience `QuotaExceededError` when localStorage fills up with workflow drafts, breaking auto-save functionality ## Review Focus - LRU eviction logic in `draftCacheV2.ts` - Quota error handling and recovery in `storageIO.ts` --- *Part 2 of 4 in the workflow persistence improvements stack* --------- Co-authored-by: Amp <amp@ampcode.com>

Summary
Adds an LRU (Least Recently Used) cache layer and storage I/O utilities that handle localStorage quota limits gracefully. When storage is full, the oldest drafts are automatically evicted to make room for new ones.
Changes
draftCacheV2.ts- In-memory LRU cache with configurable max entries (default 32)storageIO.ts- Storage read/write with automatic quota management and evictionQuotaExceededErrorwhen localStorage fills up with workflow drafts, breaking auto-save functionalityReview Focus
draftCacheV2.tsstorageIO.tsPart 2 of 4 in the workflow persistence improvements stack