Files
Netcatty/domain/connectionLogTerminalData.ts
T
陈大猫 5dad3d0e65 fix(logs): preserve connection log terminal replay data (#1733)
* fix(logs): preserve connection log terminal replay data (#1733)

Persist unsaved terminal replay buffers in a side store and merge on reload so pruned localStorage writes and cross-window sync no longer wipe captured session output. Also bind hotkey local terminals to sessionId and match captures strictly by session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): merge side-store writes and handle quota failures (#1733)

Fold existing localStorage terminal replay data before persisting the side store so multi-window writes do not clobber each other. Skip pruning the main connection-log blob when the side-store write fails so unsaved replay data is not dropped from disk.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): retain orphan side-store entries across window sync (#1733)

Keep terminal replay buffers for log ids not yet present in this window's connection log snapshot, and warn when the main blob fallback write also fails under quota pressure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): gate orphan side-store retention on persisted log ids (#1733)

Only retain side-store replay buffers for log ids still present in the persisted connection-log blob, and write the pruned main blob before syncing the side store so deletions are not resurrected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): prefer fresher local data when retaining orphan replays (#1733)

When restoring side-store entries for log ids not yet in memory, use the merged local/ref snapshot instead of stale on-disk side-store bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(logs): skip orphan side-store sync when main blob write fails (#1733)

Avoid resurrecting deleted replay buffers from a stale persisted log-id set when the pruned connection-log blob could not be written.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 00:02:12 +08:00

116 lines
3.6 KiB
TypeScript

import type { ConnectionLog } from "./models.ts";
/** Max unsaved connection logs whose terminal replay data we persist separately. */
export const MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES = 50;
export type ConnectionLogTerminalDataMap = Record<string, string>;
export const readTerminalDataFromLog = (log: ConnectionLog): string | undefined =>
log.terminalData;
export const mergeTerminalDataIntoLogs = (
logs: ConnectionLog[],
terminalDataMap: ConnectionLogTerminalDataMap,
): ConnectionLog[] => {
if (logs.length === 0) return logs;
let changed = false;
const next = logs.map((log) => {
if (log.terminalData) return log;
const sideData = terminalDataMap[log.id];
if (!sideData) return log;
changed = true;
return { ...log, terminalData: sideData };
});
return changed ? next : logs;
};
/**
* When another window or a pruned localStorage write reloads connection logs,
* keep in-memory / side-store terminal replay data instead of wiping it.
*/
export const mergeConnectionLogsFromStorage = (
prev: ConnectionLog[],
next: ConnectionLog[],
terminalDataMap: ConnectionLogTerminalDataMap,
): ConnectionLog[] => {
if (next.length === 0) return next;
const prevById = new Map(prev.map((log) => [log.id, log]));
let changed = false;
const merged = next.map((log) => {
const memoryData = readTerminalDataFromLog(prevById.get(log.id) ?? log);
const sideData = terminalDataMap[log.id];
const terminalData = memoryData ?? readTerminalDataFromLog(log) ?? sideData;
if (!terminalData || log.terminalData === terminalData) return log;
changed = true;
return { ...log, terminalData };
});
return changed ? merged : next;
};
export const buildTerminalDataMapFromLogs = (
logs: ConnectionLog[],
): ConnectionLogTerminalDataMap => {
const map: ConnectionLogTerminalDataMap = {};
for (const log of logs) {
const data = readTerminalDataFromLog(log);
if (data) map[log.id] = data;
}
return map;
};
/**
* Keep only unsaved logs' terminal data, capped to the most recent entries.
* Saved logs keep terminalData in the main connection log blob when bookmarked.
*/
export const pruneTerminalDataMapForStorage = (
logs: ConnectionLog[],
map: ConnectionLogTerminalDataMap,
): ConnectionLogTerminalDataMap => {
const unsavedIds = logs
.filter((log) => !log.saved)
.sort((a, b) => b.startTime - a.startTime)
.slice(0, MAX_PERSISTED_UNSAVED_TERMINAL_DATA_ENTRIES)
.map((log) => log.id);
const allowed = new Set(unsavedIds);
for (const log of logs) {
if (log.saved && map[log.id]) {
allowed.add(log.id);
}
}
const next: ConnectionLogTerminalDataMap = {};
for (const id of allowed) {
if (map[id]) next[id] = map[id];
}
return next;
};
/** Fold side-store maps together, then cap to the allowed unsaved/saved set. */
export const mergeTerminalDataMapsForStorage = (
logs: ConnectionLog[],
persistedSnapshot: ConnectionLogTerminalDataMap,
localMaps: ConnectionLogTerminalDataMap[],
persistedLogIds: ReadonlySet<string>,
): ConnectionLogTerminalDataMap => {
const combined: ConnectionLogTerminalDataMap = { ...persistedSnapshot };
for (const map of localMaps) {
for (const [id, data] of Object.entries(map)) {
if (data) combined[id] = data;
}
}
const pruned = pruneTerminalDataMapForStorage(logs, combined);
// Retain replay buffers only for log ids still present in the persisted
// connection-log blob but not yet loaded into this window's React state.
for (const id of persistedLogIds) {
if (!logs.some((log) => log.id === id) && combined[id]) {
pruned[id] = combined[id];
}
}
return pruned;
};