Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 8af458c8e0 perf(vault): align in-memory connection-log terminalData with the 50-entry persist cap
Unsaved logs kept up to 500×1M chars of terminalData in React state while
only 50 survive a restart. After add/update/toggle, drop terminalData from
the oldest unsaved entries beyond MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES.
List metadata stays; replay beyond 50 matches restart behaviour.

When the side-store write fails (QuotaExceeded once it nears ~50 MB), keep
the same capped merged map in memory instead of accumulating every log's
replay buffer in connectionLogTerminalDataRef.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-22 06:42:28 +00:00
3 changed files with 80 additions and 14 deletions

No files matched your search

+15 -14
View File
@@ -64,6 +64,7 @@ import {
mergeConnectionLogsFromStorage,
mergeTerminalDataIntoLogs,
mergeTerminalDataMapsForStorage,
pruneInMemoryUnsavedTerminalData,
type ConnectionLogTerminalDataMap,
} from "../../domain/connectionLogTerminalData";
import { getNextVaultOrder, normalizeVaultOrder } from "../../domain/vaultOrder";
@@ -450,14 +451,10 @@ export const useVaultState = () => {
buildPersistedLogIds(logs, usePersistedLogIdsFromDisk),
);
const persisted = writeConnectionLogTerminalDataMap(existing, merged);
if (persisted) {
connectionLogTerminalDataRef.current = merged;
} else {
connectionLogTerminalDataRef.current = {
...connectionLogTerminalDataRef.current,
...buildTerminalDataMapFromLogs(logs),
};
}
// On a failed write (e.g. QuotaExceeded once the side store nears ~50 MB)
// keep the same capped map instead of accumulating every log's replay
// buffer in memory; anything outside it would be pruned on the next write.
connectionLogTerminalDataRef.current = merged;
return { map: connectionLogTerminalDataRef.current, persisted };
}, []);
@@ -1305,8 +1302,10 @@ export const useVaultState = () => {
const savedLogs = prev.filter((l) => l.saved);
const unsavedLogs = prev.filter((l) => !l.saved);
const updated = [newLog, ...unsavedLogs].slice(0, 500);
const final = [...updated, ...savedLogs].sort(
(a, b) => b.startTime - a.startTime
const final = pruneInMemoryUnsavedTerminalData(
[...updated, ...savedLogs].sort(
(a, b) => b.startTime - a.startTime
),
);
persistConnectionLogState(final, { pruneMainBlob: true });
return final;
@@ -1319,8 +1318,8 @@ export const useVaultState = () => {
const updateConnectionLog = useCallback(
(id: string, updates: Partial<ConnectionLog>) => {
setConnectionLogs((prev) => {
const updated = prev.map((log) =>
log.id === id ? { ...log, ...updates } : log
const updated = pruneInMemoryUnsavedTerminalData(
prev.map((log) => (log.id === id ? { ...log, ...updates } : log)),
);
persistConnectionLogState(updated, { pruneMainBlob: true });
return updated;
@@ -1331,8 +1330,10 @@ export const useVaultState = () => {
const toggleConnectionLogSaved = useCallback((id: string) => {
setConnectionLogs((prev) => {
const updated = prev.map((log) =>
log.id === id ? { ...log, saved: !log.saved } : log
const updated = pruneInMemoryUnsavedTerminalData(
prev.map((log) =>
log.id === id ? { ...log, saved: !log.saved } : log
),
);
persistConnectionLogState(updated, { pruneMainBlob: false });
return updated;
+39
View File
@@ -8,6 +8,7 @@ import {
mergeConnectionLogsFromStorage,
mergeTerminalDataIntoLogs,
mergeTerminalDataMapsForStorage,
pruneInMemoryUnsavedTerminalData,
pruneTerminalDataMapForStorage,
} from "./connectionLogTerminalData.ts";
@@ -149,6 +150,44 @@ test("pruneTerminalDataMapForStorage caps unsaved replay buffers", () => {
assert.equal(pruned["log-0"], undefined);
});
test("pruneInMemoryUnsavedTerminalData drops oldest unsaved replay buffers beyond the persist cap", () => {
const logs: ConnectionLog[] = Array.from({ length: 60 }, (_, index) => ({
...baseLog,
id: `log-${index}`,
startTime: index,
saved: false,
terminalData: `data-${index}`,
}));
// One bookmarked entry must keep its replay buffer even when unsaved count is high.
logs.push({
...baseLog,
id: "saved-keep",
startTime: 0,
saved: true,
terminalData: "saved-data",
});
const pruned = pruneInMemoryUnsavedTerminalData(logs);
const unsavedWithData = pruned.filter((log) => !log.saved && log.terminalData !== undefined);
assert.equal(unsavedWithData.length, MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES);
assert.equal(pruned.find((log) => log.id === "log-59")?.terminalData, "data-59");
assert.equal(pruned.find((log) => log.id === "log-0")?.terminalData, undefined);
assert.equal(pruned.find((log) => log.id === "log-0")?.id, "log-0"); // metadata kept
assert.equal(pruned.find((log) => log.id === "saved-keep")?.terminalData, "saved-data");
assert.equal(pruned.length, logs.length);
});
test("pruneInMemoryUnsavedTerminalData is a no-op at or under the cap", () => {
const logs: ConnectionLog[] = Array.from({ length: 10 }, (_, index) => ({
...baseLog,
id: `log-${index}`,
startTime: index,
terminalData: `data-${index}`,
}));
const pruned = pruneInMemoryUnsavedTerminalData(logs);
assert.equal(pruned, logs);
});
test("mergeTerminalDataMapsForStorage keeps replay data from other windows", () => {
const logs: ConnectionLog[] = [
{ ...baseLog, id: "local", startTime: 2000 },
+26
View File
@@ -88,6 +88,32 @@ export const pruneTerminalDataMapForStorage = (
return next;
};
/**
* Drop in-memory `terminalData` from the oldest unsaved logs beyond the
* persisted-entry cap. List metadata stays; only replay buffers are removed.
* Matches what survives a restart (`pruneTerminalDataMapForStorage`).
*/
export const pruneInMemoryUnsavedTerminalData = (
logs: ConnectionLog[],
maxEntries: number = MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES,
): ConnectionLog[] => {
const unsavedWithData = logs
.filter((log) => !log.saved && log.terminalData !== undefined)
.sort((a, b) => b.startTime - a.startTime);
if (unsavedWithData.length <= maxEntries) return logs;
const keep = new Set(unsavedWithData.slice(0, maxEntries).map((log) => log.id));
let changed = false;
const next = logs.map((log) => {
if (log.saved || log.terminalData === undefined || keep.has(log.id)) return log;
changed = true;
const { terminalData: _omitted, ...rest } = log;
return rest;
});
return changed ? next : logs;
};
/** Fold side-store maps together, then cap to the allowed unsaved/saved set. */
export const mergeTerminalDataMapsForStorage = (
logs: ConnectionLog[],