From 120c717035c7888c787b61bc1bdc9cfcf8d4f1f6 Mon Sep 17 00:00:00 2001 From: mektpoy Date: Fri, 23 Oct 2026 22:01:41 +0800 Subject: [PATCH] fix(experimental): tighten paging guards and event payload typing --- .../client-ui-perf/src/client/PerfView.tsx | 2 +- .../client-ui-perf/src/client/index.ts | 28 ++++++--------- .../client-ui-perf/src/client/perf-event.ts | 34 +++++++++++-------- .../tests/perf-plugin.client.spec.ts | 4 +-- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/experimental/client-ui-perf/src/client/PerfView.tsx b/packages/experimental/client-ui-perf/src/client/PerfView.tsx index e863acafa7..82fa99892a 100644 --- a/packages/experimental/client-ui-perf/src/client/PerfView.tsx +++ b/packages/experimental/client-ui-perf/src/client/PerfView.tsx @@ -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) diff --git a/packages/experimental/client-ui-perf/src/client/index.ts b/packages/experimental/client-ui-perf/src/client/index.ts index 16794bb92a..c56e00ef1f 100644 --- a/packages/experimental/client-ui-perf/src/client/index.ts +++ b/packages/experimental/client-ui-perf/src/client/index.ts @@ -102,23 +102,17 @@ export function apply(ctx: Context): void { if (pending !== undefined) return pending const task = (async (): Promise => { 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) diff --git a/packages/experimental/client-ui-perf/src/client/perf-event.ts b/packages/experimental/client-ui-perf/src/client/perf-event.ts index 80c7fa6948..f9582032fa 100644 --- a/packages/experimental/client-ui-perf/src/client/perf-event.ts +++ b/packages/experimental/client-ui-perf/src/client/perf-event.ts @@ -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[0] -/** Payload of an event whose type is known to the caller. */ -function dataOf(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(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(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(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 diff --git a/packages/experimental/client-ui-perf/tests/perf-plugin.client.spec.ts b/packages/experimental/client-ui-perf/tests/perf-plugin.client.spec.ts index 2450fc7d0f..1a30434d03 100644 --- a/packages/experimental/client-ui-perf/tests/perf-plugin.client.spec.ts +++ b/packages/experimental/client-ui-perf/tests/perf-plugin.client.spec.ts @@ -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() - 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)