fix(experimental): tighten paging guards and event payload typing
This commit is contained in:
1 parent
65b7e61b76
commit
120c717035
4 files changed
+32
-36
No files matched your search
@@ -1734,7 +1734,7 @@ function stepRows(options: StepRowsOptions): ReactNode[] {
|
||||
const gap = gaps.get(step.step)
|
||||
const visibleGap = gap !== undefined && gap >= STEP_GAP_MIN_MS ? gap : undefined
|
||||
const expanded = open[step.step] === true
|
||||
const hasAnswer = step.answerBody !== null && step.answerBody !== undefined
|
||||
const hasAnswer = step.answerBody !== null
|
||||
const hasDetail = step.calls.length > 0 || hasAnswer
|
||||
const rowClass: (string | undefined)[] = []
|
||||
if (hasDetail) rowClass.push(css.stepRowClickable)
|
||||
|
||||
@@ -102,23 +102,17 @@ export function apply(ctx: Context): void {
|
||||
if (pending !== undefined) return pending
|
||||
const task = (async (): Promise<LoadOlderResult> => {
|
||||
const before = perf.getSnapshot()
|
||||
let changed = false
|
||||
const stop = perf.subscribe(() => { changed = true })
|
||||
try {
|
||||
// A view can mount before the Session finishes opening; paging before
|
||||
// that is a no-op, so wait for open unless the view is gone.
|
||||
await whenOpened(session, lifecycle.signal)
|
||||
if (lifecycle.signal.aborted) return 'retry'
|
||||
if (session.getSnapshot().loadingOlder) return 'retry'
|
||||
await session.loadOlder()
|
||||
if (changed || perf.getSnapshot() !== before) return 'grew'
|
||||
const after = session.getSnapshot()
|
||||
// A page can grow the Session window without adding a perf node. Only
|
||||
// an exhausted Session may hide the trigger; otherwise keep it retryable.
|
||||
return after.hasMore ? 'retry' : 'exhausted'
|
||||
} finally {
|
||||
stop()
|
||||
}
|
||||
// A view can mount before the Session finishes opening; paging before
|
||||
// that is a no-op, so wait for open unless the view is gone.
|
||||
await whenOpened(session, lifecycle.signal)
|
||||
if (lifecycle.signal.aborted) return 'retry'
|
||||
if (session.getSnapshot().loadingOlder) return 'retry'
|
||||
await session.loadOlder()
|
||||
if (perf.getSnapshot() !== before) return 'grew'
|
||||
const after = session.getSnapshot()
|
||||
// A page can grow the Session window without adding a perf node. Only
|
||||
// an exhausted Session may hide the trigger; otherwise keep it retryable.
|
||||
return after.hasMore ? 'retry' : 'exhausted'
|
||||
})()
|
||||
const promise = task.finally(() => { pages.delete(binding) })
|
||||
pages.set(binding, promise)
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-compaction/types'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
|
||||
|
||||
import type {} from '@deepseek-ai/dsh-user-approval/types'
|
||||
import type { SessionEventMap } from '@deepseek-ai/dsh-session/types'
|
||||
import type { PerfEventMetrics } from './perf-contract.ts'
|
||||
@@ -13,9 +13,15 @@ import type { PerfEventMetrics } from './perf-contract.ts'
|
||||
/** Event shape a Conversation Definition receives. */
|
||||
type MatchEvent = Parameters<ConversationNodeDefinition['match']>[0]
|
||||
|
||||
/** Payload of an event whose type is known to the caller. */
|
||||
function dataOf<T>(event: MatchEvent): T {
|
||||
return (event as MatchEvent & { data: T }).data
|
||||
/**
|
||||
* Payload of an event whose type is known to the caller.
|
||||
* @param event - event seen by the Definition match.
|
||||
* @param type - event discriminant already narrowed by the caller.
|
||||
* @returns the typed event payload.
|
||||
*/
|
||||
function dataOf<K extends keyof SessionEventMap>(event: MatchEvent, type: K): SessionEventMap[K] {
|
||||
void type
|
||||
return (event as MatchEvent & { type: K; data: SessionEventMap[K] }).data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +119,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
}
|
||||
const eventType: string = event.type
|
||||
if (eventType === 'request/context') {
|
||||
const data = dataOf<SessionEventMap['request/context']>(event)
|
||||
const data = dataOf(event, 'request/context')
|
||||
return {
|
||||
...base,
|
||||
kind: 'request-context',
|
||||
@@ -124,7 +130,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
}
|
||||
}
|
||||
if (eventType === 'llm/retry') {
|
||||
const data = dataOf<LlmRetryEventData>(event)
|
||||
const data = dataOf(event, 'llm/retry')
|
||||
return {
|
||||
...base,
|
||||
kind: 'model-retry',
|
||||
@@ -138,7 +144,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
}
|
||||
}
|
||||
if (eventType === 'assistant/attempt') {
|
||||
const data = dataOf<{ turn: number; step: number }>(event)
|
||||
const data = dataOf(event, 'assistant/attempt')
|
||||
return {
|
||||
...base,
|
||||
kind: 'model-attempt',
|
||||
@@ -148,20 +154,18 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
}
|
||||
}
|
||||
if (eventType === 'user/message') {
|
||||
const data = dataOf<{
|
||||
source: { kind: string }
|
||||
content: readonly { type: string; text?: string }[]
|
||||
}>(event)
|
||||
if (data.source.kind !== 'runtime-context' && data.source.kind !== 'time-context') return null
|
||||
const data = dataOf(event, 'user/message')
|
||||
const kind: string = data.source.kind
|
||||
if (kind !== 'runtime-context' && kind !== 'time-context') return null
|
||||
return {
|
||||
...base,
|
||||
kind: 'context-refresh',
|
||||
status: data.source.kind,
|
||||
status: kind,
|
||||
chars: contentChars(data.content),
|
||||
}
|
||||
}
|
||||
if (eventType === 'approval/asked') {
|
||||
const data = dataOf<{ toolName: string; reason?: string }>(event)
|
||||
const data = dataOf(event, 'approval/asked')
|
||||
return {
|
||||
...base,
|
||||
kind: 'approval',
|
||||
@@ -171,7 +175,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
|
||||
}
|
||||
}
|
||||
if (eventType === 'approval/decided') {
|
||||
const data = dataOf<{ outcome: string }>(event)
|
||||
const data = dataOf(event, 'approval/decided')
|
||||
return { ...base, kind: 'approval', status: data.outcome }
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -450,14 +450,12 @@ describe('browser plugin registration', () => {
|
||||
const injectEntry = bench.entries()[0]!.inject as ((sessionId: SessionId) => PerfViewInjected) | undefined
|
||||
const injected = injectEntry!(SID)
|
||||
bench.setHasMore(true)
|
||||
const gate = Promise.withResolvers<undefined>()
|
||||
bench.loadOlder.mockImplementationOnce(() => gate.promise)
|
||||
bench.loadOlder.mockImplementationOnce(() => undefined)
|
||||
const first = injected.loadOlder()
|
||||
const second = injected.loadOlder()
|
||||
expect(second).toBe(first)
|
||||
await Promise.resolve()
|
||||
expect(bench.loadOlder).toHaveBeenCalledTimes(1)
|
||||
gate.resolve(undefined)
|
||||
expect(await first).toBe('retry')
|
||||
expect(bench.loadOlder).toHaveBeenCalledTimes(1)
|
||||
|
||||
|
||||
Reference in new issue
Block a user