Files
openchamber/packages/ui/src/sync/live-aggregate.ts
T

213 lines
5.4 KiB
TypeScript

import type { SessionStatus } from '@opencode-ai/sdk/v2/client'
import type { Session } from '@opencode-ai/sdk/v2'
import type { State } from './types'
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
}
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 (leftStatus?.type !== rightStatus?.type) {
return false
}
if (getStatusMessage(leftStatus) !== getStatusMessage(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) {
for (const sessionId of Object.keys(state.session_status ?? {})) {
const next = getStatusCandidate(state, sessionId)
if (!next) {
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
}
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
}