fix(app): resolve target session lineage outside router transition (#34838)
This commit is contained in:
parent
a39db9c6f6
commit
39dfbb53d0
4 changed files with 243 additions and 35 deletions
|
|
@ -11,7 +11,7 @@ import {
|
|||
createMemo,
|
||||
createEffect,
|
||||
createComputed,
|
||||
createResource,
|
||||
createSignal,
|
||||
on,
|
||||
onMount,
|
||||
type ParentProps,
|
||||
|
|
@ -86,7 +86,7 @@ import { diffs as list } from "@/utils/diffs"
|
|||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { formatServerError, isSessionNotFoundError } from "@/utils/server-errors"
|
||||
import { legacySessionHref, requireServerKey, selectSessionLineage, sessionHref } from "@/utils/session-route"
|
||||
import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
|
||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||
import { createSessionOwnership } from "./session/session-ownership"
|
||||
|
||||
|
|
@ -224,17 +224,8 @@ function ResolvedTargetSessionRoute() {
|
|||
const params = useParams<{ serverKey: string; id: string }>()
|
||||
const settings = useSettings()
|
||||
const tabs = useTabs()
|
||||
const sync = useServerSync()
|
||||
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
||||
const cached = createMemo(() => sync().session.lineage.peek(params.id))
|
||||
const [resolved] = createResource(
|
||||
() => {
|
||||
if (cached()) return
|
||||
return { id: params.id, sync: sync() }
|
||||
},
|
||||
({ id, sync }) => sync.session.lineage.resolve(id),
|
||||
)
|
||||
const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved()))
|
||||
const current = createSessionLineage(() => params.id)
|
||||
const directory = createMemo(() => current()?.session.directory)
|
||||
const targetDirectory = () => directory()!
|
||||
|
||||
|
|
@ -265,6 +256,42 @@ function ResolvedTargetSessionRoute() {
|
|||
)
|
||||
}
|
||||
|
||||
// Reactive session lineage for the target session route, read from the sync store.
|
||||
// The route keys its consumer to the session ID, so resolution runs once per target.
|
||||
// Resolution is imperative rather than a resource on purpose: a resource created here
|
||||
// would be created inside the router's navigation transition, and suspending that
|
||||
// transition deadlocks the URL commit and double-mounts the session header portals
|
||||
// from the transition's shadow render. `lineage.resolve` fills the sync store, which
|
||||
// the returned accessor observes; resolve failures rethrow on read so the enclosing
|
||||
// SessionRouteErrorBoundary renders the scoped session error.
|
||||
function createSessionLineage(sessionID: () => string) {
|
||||
const sync = useServerSync()
|
||||
const cached = createMemo(() => sync().session.lineage.peek(sessionID()))
|
||||
const [failure, setFailure] = createSignal<unknown>()
|
||||
const [settled, setSettled] = createSignal(false)
|
||||
onMount(() => {
|
||||
if (cached()) {
|
||||
setSettled(true)
|
||||
return
|
||||
}
|
||||
sync()
|
||||
.session.lineage.resolve(sessionID())
|
||||
.then(() => setSettled(true))
|
||||
.catch((error) => setFailure(() => error))
|
||||
})
|
||||
return createMemo(() => {
|
||||
const error = failure()
|
||||
if (error) throw error
|
||||
const lineage = cached()
|
||||
// The viewed session is pinned and pinned lineages are exempt from cache pruning,
|
||||
// so a lineage missing after settlement means the session (or an ancestor) was
|
||||
// deleted, possibly by another client. Match the resolve error so the boundary
|
||||
// shows the session not found fallback.
|
||||
if (!lineage && settled()) throw new Error(`Session not found: ${sessionID()}`)
|
||||
return lineage
|
||||
})
|
||||
}
|
||||
|
||||
function TargetSessionPage() {
|
||||
const sdk = useSDK()
|
||||
const serverSDK = useServerSDK()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import {
|
||||
legacySessionHref,
|
||||
legacySessionServer,
|
||||
requireServerKey,
|
||||
rootSession,
|
||||
selectSessionLineage,
|
||||
sessionHref,
|
||||
} from "./session-route"
|
||||
import { legacySessionHref, legacySessionServer, requireServerKey, rootSession, sessionHref } from "./session-route"
|
||||
|
||||
describe("session routes", () => {
|
||||
test("uses the unique persisted server for a legacy session route", () => {
|
||||
|
|
@ -75,10 +68,4 @@ describe("session routes", () => {
|
|||
|
||||
expect(rootSession(sessions.child, async (id) => sessions[id]!)).rejects.toThrow("Session parent cycle: child")
|
||||
})
|
||||
|
||||
test("ignores a resolved lineage retained from the previous route", () => {
|
||||
const previous = { session: { id: "A" }, root: { id: "A" } }
|
||||
|
||||
expect(selectSessionLineage("B", undefined, previous)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,15 +27,6 @@ export function legacySessionServer(
|
|||
|
||||
type SessionParent = { id: string; parentID?: string }
|
||||
|
||||
export function selectSessionLineage<T extends { session: { id: string } }>(
|
||||
sessionID: string,
|
||||
cached: T | undefined,
|
||||
resolved: T | undefined,
|
||||
) {
|
||||
if (cached?.session.id === sessionID) return cached
|
||||
if (resolved?.session.id === sessionID) return resolved
|
||||
}
|
||||
|
||||
export async function rootSession<T extends SessionParent>(session: T, get: (sessionID: string) => Promise<T>) {
|
||||
const seen = new Set([session.id])
|
||||
let current = session
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue