perf(ui-trajectory): reuse finalized stream projections
This commit is contained in:
@@ -41,6 +41,12 @@ describe('PartialAccumulator', () => {
|
||||
expect(acc.toPartial().blocks).toEqual([{ kind: 'reasoning', text: '思考' }])
|
||||
})
|
||||
|
||||
it('continues from a materialized history prefix', () => {
|
||||
const acc = new PartialAccumulator(1, 0, [{ kind: 'text', text: '已有' }])
|
||||
acc.push(chunk({ type: 'text-delta', index: 0, text: '增量' }))
|
||||
expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '已有增量' }])
|
||||
})
|
||||
|
||||
it('folds tool-call deltas: first id pins callId, late name overrides, argsRaw concatenates', () => {
|
||||
const acc = new PartialAccumulator(1, 0)
|
||||
acc.push(chunk({ type: 'tool-call-delta', index: 0, id: 'c1', argumentsDelta: '{"a"' }))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { SessionHistorySource } from '../src/client/session-history/source.ts'
|
||||
@@ -7,6 +7,10 @@ import { entries, ev, plainTurn } from './event-script.ts'
|
||||
|
||||
const SID = 'history-s1' as SessionId
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function histResponse(events: SessionEvent[], hasMore = false) {
|
||||
return Promise.resolve(ok({ events: entries(events) as never[], hasMore }))
|
||||
}
|
||||
@@ -52,6 +56,71 @@ describe('SessionHistorySource', () => {
|
||||
.toEqual([1, 3, 6])
|
||||
})
|
||||
|
||||
it('publishes multiple assistant chunks once per browser frame', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = () => histResponse(plainTurn(0, 0, '问', '答'))
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
await source.loadAll()
|
||||
const frames: FrameRequestCallback[] = []
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||
frames.push(callback)
|
||||
return frames.length
|
||||
})
|
||||
let notifications = 0
|
||||
const unsubscribe = source.subscribe(() => { notifications++ })
|
||||
const before = source.getSnapshot().inspection
|
||||
const finalizedNodes = before.eventNodes
|
||||
const requests = before.requests
|
||||
const contexts = before.contexts
|
||||
|
||||
for (const event of [
|
||||
ev.chunkStart(6, 1),
|
||||
ev.chunkText(7, 1, 'stream '),
|
||||
ev.chunkText(8, 1, 'content'),
|
||||
]) {
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
expect(frames).toHaveLength(1)
|
||||
expect(notifications).toBe(0)
|
||||
frames[0]?.(0)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(notifications).toBe(1)
|
||||
const streamed = source.getSnapshot().inspection
|
||||
expect(streamed.eventNodes).toBe(finalizedNodes)
|
||||
expect(streamed.requests).toBe(requests)
|
||||
expect(streamed.contexts).toBe(contexts)
|
||||
expect(streamed.partial?.blocks).toEqual([
|
||||
{ kind: 'text', text: 'stream content' },
|
||||
])
|
||||
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: ev.chunkText(9, 1, ' then final'),
|
||||
})
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: ev.assistant(10, 1, 'stream content then final'),
|
||||
})
|
||||
await Promise.resolve()
|
||||
|
||||
expect(notifications).toBe(2)
|
||||
const finalized = source.getSnapshot().inspection
|
||||
expect(finalized.eventNodes).not.toBe(finalizedNodes)
|
||||
expect(finalized.partial).toBeNull()
|
||||
frames[1]?.(0)
|
||||
await Promise.resolve()
|
||||
expect(notifications).toBe(2)
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('stops loading when an older page fails to advance', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = payload => payload.beforeSeq === undefined
|
||||
|
||||
Reference in New Issue
Block a user