mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-05 21:20:12 +00:00
feat(persistence): add LRU draft cache with quota management (#8518)
## 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>
This commit is contained in:
193
src/platform/workflow/persistence/base/draftCacheV2.ts
Normal file
193
src/platform/workflow/persistence/base/draftCacheV2.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* V2 Draft Cache - Pure functions for draft index manipulation.
|
||||
*
|
||||
* This module provides immutable operations on the draft index structure.
|
||||
* All functions return new objects rather than mutating inputs.
|
||||
*/
|
||||
|
||||
import type { DraftEntryMeta, DraftIndexV2 } from './draftTypes'
|
||||
import { MAX_DRAFTS } from './draftTypes'
|
||||
import { hashPath } from './hashUtil'
|
||||
|
||||
/**
|
||||
* Creates an empty draft index.
|
||||
*/
|
||||
export function createEmptyIndex(): DraftIndexV2 {
|
||||
return {
|
||||
v: 2,
|
||||
updatedAt: Date.now(),
|
||||
order: [],
|
||||
entries: {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a draft key to the end of the LRU order (most recently used).
|
||||
*/
|
||||
export function touchOrder(order: string[], draftKey: string): string[] {
|
||||
const filtered = order.filter((key) => key !== draftKey)
|
||||
return [...filtered, draftKey]
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates a draft entry in the index.
|
||||
* Handles LRU eviction if over the limit.
|
||||
*
|
||||
* @returns Object with updated index and list of evicted draft keys
|
||||
*/
|
||||
export function upsertEntry(
|
||||
index: DraftIndexV2,
|
||||
path: string,
|
||||
meta: Omit<DraftEntryMeta, 'path'>,
|
||||
limit: number = MAX_DRAFTS
|
||||
): { index: DraftIndexV2; evicted: string[] } {
|
||||
const draftKey = hashPath(path)
|
||||
const effectiveLimit = Math.max(1, limit)
|
||||
|
||||
const entries = {
|
||||
...index.entries,
|
||||
[draftKey]: { ...meta, path }
|
||||
}
|
||||
|
||||
const order = touchOrder(index.order, draftKey)
|
||||
const evicted: string[] = []
|
||||
|
||||
while (order.length > effectiveLimit) {
|
||||
const oldest = order.shift()
|
||||
if (oldest && oldest !== draftKey) {
|
||||
delete entries[oldest]
|
||||
evicted.push(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
index: {
|
||||
v: 2,
|
||||
updatedAt: Date.now(),
|
||||
order,
|
||||
entries
|
||||
},
|
||||
evicted
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a draft entry from the index.
|
||||
*
|
||||
* @returns Object with updated index and the removed draft key (if any)
|
||||
*/
|
||||
export function removeEntry(
|
||||
index: DraftIndexV2,
|
||||
path: string
|
||||
): { index: DraftIndexV2; removedKey: string | null } {
|
||||
const draftKey = hashPath(path)
|
||||
|
||||
if (!(draftKey in index.entries)) {
|
||||
return { index, removedKey: null }
|
||||
}
|
||||
|
||||
const entries = { ...index.entries }
|
||||
delete entries[draftKey]
|
||||
|
||||
return {
|
||||
index: {
|
||||
v: 2,
|
||||
updatedAt: Date.now(),
|
||||
order: index.order.filter((key) => key !== draftKey),
|
||||
entries
|
||||
},
|
||||
removedKey: draftKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a draft from one path to another (rename operation).
|
||||
*
|
||||
* @returns Object with updated index and keys involved
|
||||
*/
|
||||
export function moveEntry(
|
||||
index: DraftIndexV2,
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
newName: string
|
||||
): { index: DraftIndexV2; oldKey: string; newKey: string } | null {
|
||||
const oldKey = hashPath(oldPath)
|
||||
const newKey = hashPath(newPath)
|
||||
|
||||
const oldEntry = index.entries[oldKey]
|
||||
if (!oldEntry) return null
|
||||
if (oldKey !== newKey && index.entries[newKey]) return null
|
||||
|
||||
const entries = { ...index.entries }
|
||||
delete entries[oldKey]
|
||||
|
||||
entries[newKey] = {
|
||||
...oldEntry,
|
||||
path: newPath,
|
||||
name: newName,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
|
||||
const order = index.order
|
||||
.filter((key) => key !== oldKey && key !== newKey)
|
||||
.concat(newKey)
|
||||
|
||||
return {
|
||||
index: {
|
||||
v: 2,
|
||||
updatedAt: Date.now(),
|
||||
order,
|
||||
entries
|
||||
},
|
||||
oldKey,
|
||||
newKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recently used draft key.
|
||||
*/
|
||||
export function getMostRecentKey(index: DraftIndexV2): string | null {
|
||||
return index.order.length > 0 ? index.order[index.order.length - 1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entry metadata by path.
|
||||
*/
|
||||
export function getEntryByPath(
|
||||
index: DraftIndexV2,
|
||||
path: string
|
||||
): DraftEntryMeta | null {
|
||||
const draftKey = hashPath(path)
|
||||
return index.entries[draftKey] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes entries from index that don't have corresponding payloads.
|
||||
* Used for index/payload drift recovery.
|
||||
*
|
||||
* @param index - The draft index
|
||||
* @param existingPayloadKeys - Set of draft keys that have payloads in storage
|
||||
* @returns Updated index with orphaned entries removed
|
||||
*/
|
||||
export function removeOrphanedEntries(
|
||||
index: DraftIndexV2,
|
||||
existingPayloadKeys: Set<string>
|
||||
): DraftIndexV2 {
|
||||
const entries: Record<string, DraftEntryMeta> = {}
|
||||
const order: string[] = []
|
||||
|
||||
for (const key of index.order) {
|
||||
if (existingPayloadKeys.has(key) && index.entries[key]) {
|
||||
entries[key] = index.entries[key]
|
||||
order.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
v: 2,
|
||||
updatedAt: Date.now(),
|
||||
order,
|
||||
entries
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user