fix(sync): prefer freshest session status and persist active-now list
This commit is contained in:
@@ -57,7 +57,13 @@ import {
|
|||||||
} from './sidebar/ConfirmDialogs';
|
} from './sidebar/ConfirmDialogs';
|
||||||
import { type SessionGroup, type SessionNode } from './sidebar/types';
|
import { type SessionGroup, type SessionNode } from './sidebar/types';
|
||||||
import {
|
import {
|
||||||
|
type ActiveNowEntry,
|
||||||
|
addActiveNowSession,
|
||||||
|
deriveActiveNowSessions,
|
||||||
deriveLiveActiveNowSessions,
|
deriveLiveActiveNowSessions,
|
||||||
|
persistActiveNowEntries,
|
||||||
|
pruneActiveNowEntries,
|
||||||
|
readActiveNowEntries,
|
||||||
} from './sidebar/activitySections';
|
} from './sidebar/activitySections';
|
||||||
import {
|
import {
|
||||||
compareSessionsByPinnedAndTime,
|
compareSessionsByPinnedAndTime,
|
||||||
@@ -129,6 +135,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
() => new Map(),
|
() => new Map(),
|
||||||
);
|
);
|
||||||
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
||||||
|
const [activeNowEntries, setActiveNowEntries] = React.useState<ActiveNowEntry[]>(() => readActiveNowEntries(safeStorage));
|
||||||
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
|
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
||||||
@@ -1011,10 +1018,45 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
}, [projectSections, homeDirectory]);
|
}, [projectSections, homeDirectory]);
|
||||||
|
|
||||||
const activeNowSessions = React.useMemo(
|
const activeNowSessions = React.useMemo(
|
||||||
|
() => deriveActiveNowSessions(activeNowEntries, new Map(sessions.map((session) => [session.id, session]))),
|
||||||
|
[activeNowEntries, sessions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const liveActiveSessions = React.useMemo(
|
||||||
() => deriveLiveActiveNowSessions(sessions, liveSessionStatuses),
|
() => deriveLiveActiveNowSessions(sessions, liveSessionStatuses),
|
||||||
[liveSessionStatuses, sessions],
|
[liveSessionStatuses, sessions],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (liveActiveSessions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveNowEntries((prev) => {
|
||||||
|
const next = liveActiveSessions.reduce((entries, session) => addActiveNowSession(entries, session.id), prev);
|
||||||
|
if (next === prev) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
persistActiveNowEntries(safeStorage, next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [liveActiveSessions, safeStorage]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const allKnownSessionsById = new Map<string, Session>();
|
||||||
|
[...sessions, ...archivedSessions].forEach((session) => {
|
||||||
|
allKnownSessionsById.set(session.id, session);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pruned = pruneActiveNowEntries(activeNowEntries, allKnownSessionsById);
|
||||||
|
if (pruned.length === activeNowEntries.length && pruned.every((entry, index) => entry.sessionId === activeNowEntries[index]?.sessionId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveNowEntries(pruned);
|
||||||
|
persistActiveNowEntries(safeStorage, pruned);
|
||||||
|
}, [activeNowEntries, archivedSessions, safeStorage, sessions]);
|
||||||
|
|
||||||
// Prefetch is wired below, after recentSessionIds is computed.
|
// Prefetch is wired below, after recentSessionIds is computed.
|
||||||
|
|
||||||
const activitySections = React.useMemo(() => {
|
const activitySections = React.useMemo(() => {
|
||||||
|
|||||||
@@ -58,6 +58,27 @@ describe('live aggregate', () => {
|
|||||||
expect(findLiveSessionStatus(states, 'ses-2')?.type).toBe('retry')
|
expect(findLiveSessionStatus(states, 'ses-2')?.type).toBe('retry')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('lets a fresher idle snapshot override a stale busy status', () => {
|
||||||
|
const states = [
|
||||||
|
{
|
||||||
|
session: [session('ses-1', '/a', 10)],
|
||||||
|
session_status: {
|
||||||
|
'ses-1': { type: 'busy' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
session: [session('ses-1', '/a', 30)],
|
||||||
|
session_status: {
|
||||||
|
'ses-1': { type: 'idle' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const statuses = aggregateLiveSessionStatuses(states)
|
||||||
|
expect(statuses['ses-1']?.type).toBe('idle')
|
||||||
|
expect(findLiveSessionStatus(states, 'ses-1')?.type).toBe('idle')
|
||||||
|
})
|
||||||
|
|
||||||
it('derives active-now sessions from live statuses instead of persisted history', () => {
|
it('derives active-now sessions from live statuses instead of persisted history', () => {
|
||||||
const sessions = [
|
const sessions = [
|
||||||
session('ses-1', '/a', 20),
|
session('ses-1', '/a', 20),
|
||||||
|
|||||||
@@ -47,6 +47,36 @@ const getStatusMessage = (status: SessionStatus | undefined): string | null => {
|
|||||||
return typeof message === 'string' ? message : null
|
return typeof message === 'string' ? message : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatusCandidate = {
|
||||||
|
status: SessionStatus
|
||||||
|
sessionUpdatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusCandidate = (state: LiveStateSlice, sessionId: string): StatusCandidate | null => {
|
||||||
|
const status = state.session_status?.[sessionId]
|
||||||
|
if (!status) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = state.session.find((candidate) => candidate.id === sessionId)
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
sessionUpdatedAt: session ? getSessionUpdatedAt(session) : -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldReplaceStatusCandidate = (current: StatusCandidate | undefined, next: StatusCandidate): boolean => {
|
||||||
|
if (!current) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.sessionUpdatedAt !== current.sessionUpdatedAt) {
|
||||||
|
return next.sessionUpdatedAt > current.sessionUpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return getStatusPriority(next.status) >= getStatusPriority(current.status)
|
||||||
|
}
|
||||||
|
|
||||||
export const areSessionListsEquivalent = (left: Session[], right: Session[]): boolean => {
|
export const areSessionListsEquivalent = (left: Session[], right: Session[]): boolean => {
|
||||||
if (left === right) {
|
if (left === right) {
|
||||||
return true
|
return true
|
||||||
@@ -116,17 +146,27 @@ export function aggregateLiveSessions(states: Iterable<LiveStateSlice>): Session
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateLiveSessionStatuses(states: Iterable<LiveStateSlice>): Record<string, SessionStatus> {
|
export function aggregateLiveSessionStatuses(states: Iterable<LiveStateSlice>): Record<string, SessionStatus> {
|
||||||
const statuses: Record<string, SessionStatus> = {}
|
const candidates = new Map<string, StatusCandidate>()
|
||||||
|
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
for (const [sessionId, status] of Object.entries(state.session_status ?? {})) {
|
for (const sessionId of Object.keys(state.session_status ?? {})) {
|
||||||
const current = statuses[sessionId]
|
const next = getStatusCandidate(state, sessionId)
|
||||||
if (!current || getStatusPriority(status) >= getStatusPriority(current)) {
|
if (!next) {
|
||||||
statuses[sessionId] = status
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = candidates.get(sessionId)
|
||||||
|
if (shouldReplaceStatusCandidate(current, next)) {
|
||||||
|
candidates.set(sessionId, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statuses: Record<string, SessionStatus> = {}
|
||||||
|
for (const [sessionId, candidate] of candidates) {
|
||||||
|
statuses[sessionId] = candidate.status
|
||||||
|
}
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,16 +197,16 @@ export function findLiveSessionStatus(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
let match: SessionStatus | undefined
|
let match: StatusCandidate | undefined
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
const status = state.session_status?.[sessionID]
|
const next = getStatusCandidate(state, sessionID)
|
||||||
if (!status) {
|
if (!next) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!match || getStatusPriority(status) >= getStatusPriority(match)) {
|
if (shouldReplaceStatusCandidate(match, next)) {
|
||||||
match = status
|
match = next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return match
|
return match?.status
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user