Compare commits
1 commit
dev
...
fix-empty-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a35328aab1 |
3 changed files with 132 additions and 13 deletions
|
|
@ -1,4 +1,7 @@
|
||||||
|
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { sessionUpdatedAt, trimSessions } from "./session-trim"
|
||||||
import type { RootLoadArgs } from "./types"
|
import type { RootLoadArgs } from "./types"
|
||||||
|
import { directoryKey } from "./utils"
|
||||||
|
|
||||||
export async function loadRootSessionsWithFallback(input: RootLoadArgs) {
|
export async function loadRootSessionsWithFallback(input: RootLoadArgs) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -23,3 +26,40 @@ export function estimateRootSessionTotal(input: { count: number; limit: number;
|
||||||
if (input.count < input.limit) return input.count
|
if (input.count < input.limit) return input.count
|
||||||
return input.count + 1
|
return input.count + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeRootSessionLoad(input: {
|
||||||
|
directory: string
|
||||||
|
loadedAt: number
|
||||||
|
listed: Session[]
|
||||||
|
current: Session[]
|
||||||
|
limit: number
|
||||||
|
permission: Record<string, PermissionRequest[]>
|
||||||
|
}) {
|
||||||
|
const directory = directoryKey(input.directory)
|
||||||
|
const currentRoots = input.current.filter(
|
||||||
|
(session) => !session.parentID && !session.time?.archived && directoryKey(session.directory) === directory,
|
||||||
|
)
|
||||||
|
const byID = new Map(
|
||||||
|
input.listed
|
||||||
|
.filter((session) => !!session?.id)
|
||||||
|
.filter((session) => !session.time?.archived)
|
||||||
|
.map((session) => [session.id, session] as const),
|
||||||
|
)
|
||||||
|
|
||||||
|
// A list response can resolve after live create/update events have already seeded this store.
|
||||||
|
for (const session of currentRoots) {
|
||||||
|
const listed = byID.get(session.id)
|
||||||
|
if (!listed) {
|
||||||
|
if (sessionUpdatedAt(session) >= input.loadedAt) byID.set(session.id, session)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (sessionUpdatedAt(session) >= input.loadedAt && sessionUpdatedAt(session) > sessionUpdatedAt(listed)) {
|
||||||
|
byID.set(session.id, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimSessions([...byID.values(), ...input.current.filter((session) => !!session.parentID)], {
|
||||||
|
limit: input.limit,
|
||||||
|
permission: input.permission,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,35 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
|
import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import {
|
||||||
|
estimateRootSessionTotal,
|
||||||
|
loadRootSessionsWithFallback,
|
||||||
|
mergeRootSessionLoad,
|
||||||
|
} from "./global-sync/session-load"
|
||||||
|
|
||||||
|
function session(input: {
|
||||||
|
id: string
|
||||||
|
directory?: string
|
||||||
|
created?: number
|
||||||
|
updated?: number
|
||||||
|
parentID?: string
|
||||||
|
archived?: number
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: input.id,
|
||||||
|
directory: input.directory ?? "/repo",
|
||||||
|
projectID: "project",
|
||||||
|
title: input.id,
|
||||||
|
parentID: input.parentID,
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
time: {
|
||||||
|
created: input.created ?? 1_000,
|
||||||
|
updated: input.updated ?? input.created ?? 1_000,
|
||||||
|
archived: input.archived,
|
||||||
|
},
|
||||||
|
} as Session
|
||||||
|
}
|
||||||
|
|
||||||
describe("pickDirectoriesToEvict", () => {
|
describe("pickDirectoriesToEvict", () => {
|
||||||
test("keeps pinned stores and evicts idle stores", () => {
|
test("keeps pinned stores and evicts idle stores", () => {
|
||||||
|
|
@ -63,6 +92,48 @@ describe("loadRootSessionsWithFallback", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("mergeRootSessionLoad", () => {
|
||||||
|
test("keeps sessions created while a stale list request was in flight", () => {
|
||||||
|
const result = mergeRootSessionLoad({
|
||||||
|
directory: "/repo",
|
||||||
|
loadedAt: 2_000,
|
||||||
|
listed: [],
|
||||||
|
current: [session({ id: "new", created: 2_100 })],
|
||||||
|
limit: 5,
|
||||||
|
permission: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.map((item) => item.id)).toEqual(["new"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps newer live session data over stale list responses", () => {
|
||||||
|
const result = mergeRootSessionLoad({
|
||||||
|
directory: "/repo",
|
||||||
|
loadedAt: 2_000,
|
||||||
|
listed: [session({ id: "same", updated: 1_500 })],
|
||||||
|
current: [{ ...session({ id: "same", updated: 2_100 }), title: "new title" }],
|
||||||
|
limit: 5,
|
||||||
|
permission: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]?.title).toBe("new title")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows authoritative loads to remove old missing roots", () => {
|
||||||
|
const result = mergeRootSessionLoad({
|
||||||
|
directory: "/repo",
|
||||||
|
loadedAt: 2_000,
|
||||||
|
listed: [],
|
||||||
|
current: [session({ id: "old", updated: 1_500 })],
|
||||||
|
limit: 5,
|
||||||
|
permission: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("estimateRootSessionTotal", () => {
|
describe("estimateRootSessionTotal", () => {
|
||||||
test("keeps exact total for full fetches", () => {
|
test("keeps exact total for full fetches", () => {
|
||||||
expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42)
|
expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ import {
|
||||||
} from "./global-sync/bootstrap"
|
} from "./global-sync/bootstrap"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import {
|
||||||
|
estimateRootSessionTotal,
|
||||||
|
loadRootSessionsWithFallback,
|
||||||
|
mergeRootSessionLoad,
|
||||||
|
} from "./global-sync/session-load"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
import { trimSessions } from "./global-sync/session-trim"
|
||||||
import type { ProjectMeta } from "./global-sync/types"
|
import type { ProjectMeta } from "./global-sync/types"
|
||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
|
|
@ -273,6 +277,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(retainedLimit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(retainedLimit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
|
const loadedAt = Date.now()
|
||||||
const promise = queryClient
|
const promise = queryClient
|
||||||
.fetchQuery({
|
.fetchQuery({
|
||||||
...queryOptionsApi.sessions(key),
|
...queryOptionsApi.sessions(key),
|
||||||
|
|
@ -283,13 +288,13 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||||
list: (query) => serverSDK.client.session.list(query),
|
list: (query) => serverSDK.client.session.list(query),
|
||||||
})
|
})
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const nonArchived = (x.data ?? [])
|
const listed = (x.data ?? []).filter((s) => !!s?.id).filter((s) => !s.time?.archived)
|
||||||
.filter((s) => !!s?.id)
|
|
||||||
.filter((s) => !s.time?.archived)
|
|
||||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
||||||
const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0)
|
const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0)
|
||||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
const next = mergeRootSessionLoad({
|
||||||
const next = trimSessions([...nonArchived, ...childSessions], {
|
directory,
|
||||||
|
loadedAt,
|
||||||
|
listed,
|
||||||
|
current: store.session,
|
||||||
limit,
|
limit,
|
||||||
permission: session.data.permission,
|
permission: session.data.permission,
|
||||||
})
|
})
|
||||||
|
|
@ -297,11 +302,14 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||||
next.forEach(session.remember)
|
next.forEach(session.remember)
|
||||||
setStore(
|
setStore(
|
||||||
"sessionTotal",
|
"sessionTotal",
|
||||||
estimateRootSessionTotal({
|
Math.max(
|
||||||
count: nonArchived.length,
|
estimateRootSessionTotal({
|
||||||
limit: x.limit,
|
count: listed.length,
|
||||||
limited: x.limited,
|
limit: x.limit,
|
||||||
}),
|
limited: x.limited,
|
||||||
|
}),
|
||||||
|
next.filter((item) => !item.parentID).length,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue