Files
openchamber/packages/ui/src/sync/live-aggregate.ts
T
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.

- prioritize selected and visible sessions during bootstrap and defer
  non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
  hidden sidebar work
- prevent stale session and message requests from overwriting newer
  authoritative state
- preserve existing data when authoritative fetches fail instead of treating
  failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
  permissions, folders, tabs, Git state, and pull request data by runtime and
  directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
  persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
  message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
  bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
  runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
  rendering hot paths
- limit virtualization to archive collections where it improves rendering
  without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
  sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
  flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
  reconnect behavior, persistence races, authoritative empty results, and
  subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
  runtime-isolation invariants
2026-07-21 20:52:20 +03:00

229 lines
6.3 KiB
TypeScript

import type { SessionStatus } from '@opencode-ai/sdk/v2/client'
import type { Session } from '@opencode-ai/sdk/v2'
import type { State } from './types'
import { countSyncPerformance } from './performance-diagnostics'
type LiveStateSlice = Pick<State, 'session' | 'session_status'>
const getSessionUpdatedAt = (session: Session): number => {
const updatedAt = session.time?.updated
if (typeof updatedAt === 'number' && Number.isFinite(updatedAt)) {
return updatedAt
}
const createdAt = session.time?.created
return typeof createdAt === 'number' && Number.isFinite(createdAt) ? createdAt : 0
}
const getSessionSignature = (session: Session): string => {
const directory = (session as Session & { directory?: string | null }).directory ?? ''
const parentID = (session as Session & { parentID?: string | null }).parentID ?? ''
return [
session.id,
session.title ?? '',
session.time?.created ?? 0,
session.time?.updated ?? 0,
session.time?.archived ?? 0,
directory,
parentID,
session.share?.url ?? '',
].join('|')
}
const getStatusPriority = (status: SessionStatus | undefined): number => {
switch (status?.type) {
case 'retry':
return 4
case 'busy':
return 3
case 'idle':
return 1
default:
return 0
}
}
const getStatusMessage = (status: SessionStatus | undefined): string | null => {
const message = (status as { message?: unknown } | undefined)?.message
return typeof message === 'string' ? message : null
}
const getStatusNumberField = (status: SessionStatus | undefined, field: 'attempt' | 'next'): number | null => {
const value = (status as Record<string, unknown> | undefined)?.[field]
return typeof value === 'number' ? value : null
}
const areStatusesEquivalent = (left: SessionStatus | undefined, right: SessionStatus | undefined): boolean => {
return left?.type === right?.type
&& getStatusMessage(left) === getStatusMessage(right)
&& getStatusNumberField(left, 'attempt') === getStatusNumberField(right, 'attempt')
&& getStatusNumberField(left, 'next') === getStatusNumberField(right, 'next')
}
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 => {
if (left === right) {
return true
}
if (left.length !== right.length) {
return false
}
for (let index = 0; index < left.length; index += 1) {
if (getSessionSignature(left[index]) !== getSessionSignature(right[index])) {
return false
}
}
return true
}
export const areStatusMapsEquivalent = (
left: Record<string, SessionStatus>,
right: Record<string, SessionStatus>,
): boolean => {
if (left === right) {
return true
}
const leftKeys = Object.keys(left)
const rightKeys = Object.keys(right)
if (leftKeys.length !== rightKeys.length) {
return false
}
for (const key of leftKeys) {
if (!(key in right)) {
return false
}
const leftStatus = left[key]
const rightStatus = right[key]
if (!areStatusesEquivalent(leftStatus, rightStatus)) {
return false
}
}
return true
}
export function aggregateLiveSessions(states: Iterable<LiveStateSlice>): Session[] {
const sessionsById = new Map<string, Session>()
for (const state of states) {
for (const session of state.session) {
if (!session?.id) {
continue
}
const current = sessionsById.get(session.id)
if (!current || getSessionUpdatedAt(session) >= getSessionUpdatedAt(current)) {
sessionsById.set(session.id, session)
}
}
}
return Array.from(sessionsById.values()).sort((left, right) => {
return getSessionUpdatedAt(right) - getSessionUpdatedAt(left)
})
}
export function aggregateLiveSessionStatuses(states: Iterable<LiveStateSlice>): Record<string, SessionStatus> {
const candidates = new Map<string, StatusCandidate>()
for (const state of states) {
const sessionUpdatedAtById = new Map<string, number>()
for (const session of state.session) {
countSyncPerformance('statusAggregationSessionEntries')
sessionUpdatedAtById.set(session.id, getSessionUpdatedAt(session))
}
for (const [sessionId, status] of Object.entries(state.session_status ?? {})) {
countSyncPerformance('statusAggregationCandidates')
const next: StatusCandidate = {
status,
sessionUpdatedAt: sessionUpdatedAtById.get(sessionId) ?? -1,
}
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
}
export function findLiveSession(states: Iterable<LiveStateSlice>, sessionID?: string | null): Session | undefined {
if (!sessionID) {
return undefined
}
let match: Session | undefined
for (const state of states) {
const session = state.session.find((candidate) => candidate.id === sessionID)
if (!session) {
continue
}
if (!match || getSessionUpdatedAt(session) >= getSessionUpdatedAt(match)) {
match = session
}
}
return match
}
export function findLiveSessionStatus(
states: Iterable<LiveStateSlice>,
sessionID?: string | null,
): SessionStatus | undefined {
if (!sessionID) {
return undefined
}
let match: StatusCandidate | undefined
for (const state of states) {
const next = getStatusCandidate(state, sessionID)
if (!next) {
continue
}
if (shouldReplaceStatusCandidate(match, next)) {
match = next
}
}
return match?.status
}