Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # packages/client/runtime/tests/session.spec.ts
This commit is contained in:
20 files changed
+662
-43
No files matched your search
@@ -8,7 +8,7 @@ import type {
|
||||
} from '../contract/session-history.ts'
|
||||
import { createHistoryInspection } from '../sessions/history.ts'
|
||||
import { Notifier } from '../sessions/notifier.ts'
|
||||
import { PartialAccumulator } from '../sessions/partial.ts'
|
||||
import { isVisibleAssistantChunk, PartialAccumulator } from '../sessions/partial.ts'
|
||||
|
||||
const HISTORY_PAGE_MESSAGES = 50
|
||||
|
||||
@@ -431,11 +431,3 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
return this.inspectionCache.value
|
||||
}
|
||||
}
|
||||
|
||||
function isVisibleAssistantChunk(type: string): boolean {
|
||||
return type === 'block-start'
|
||||
|| type === 'text-delta'
|
||||
|| type === 'reasoning-delta'
|
||||
|| type === 'tool-call-delta'
|
||||
|| type === 'block-end'
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Notifier: subscription + microtask-batched notification primitive shared by Session and
|
||||
// SessionManager. Semantics: N markDirty calls collapse into one microtask flush;
|
||||
// Notifier: subscription + batched notification primitive shared by Session and
|
||||
// SessionManager. Semantics: N markDirty calls collapse into one microtask flush, while
|
||||
// N markFrameDirty calls collapse into one animation-frame flush;
|
||||
// the flush rebuilds the snapshot cache BEFORE notifying (useSyncExternalStore requires a stable
|
||||
// getSnapshot reference). With no listeners the rebuild is skipped and only the dirty bit is set
|
||||
// (keeps frame storms cheap); the next getSnapshot rebuilds lazily.
|
||||
@@ -9,12 +10,13 @@
|
||||
// swallow the notification — push subscribers (object-layer watchers) would
|
||||
// otherwise starve whenever any reader pulls first.
|
||||
|
||||
/** Subscription + microtask-batched notification primitive (shared by Session and SessionManager). */
|
||||
/** Subscription + batched notification primitive (shared by Session and SessionManager). */
|
||||
export class Notifier {
|
||||
private listeners = new Set<() => void>()
|
||||
private dirty = false
|
||||
private notifyPending = false
|
||||
private scheduled = false
|
||||
private scheduled: 'none' | 'microtask' | 'frame' = 'none'
|
||||
private scheduleGeneration = 0
|
||||
|
||||
/** @param rebuild - snapshot rebuild function injected by the owner (writes the owner's snapshotCache). */
|
||||
constructor(private readonly rebuild: () => void) {}
|
||||
@@ -35,19 +37,16 @@ export class Notifier {
|
||||
markDirty(): void {
|
||||
this.dirty = true
|
||||
this.notifyPending = true
|
||||
if (this.scheduled) return
|
||||
this.scheduled = true
|
||||
queueMicrotask(() => {
|
||||
this.scheduled = false
|
||||
if (!this.notifyPending) return
|
||||
if (this.listeners.size === 0) return // lazy: no subscribers; dirty (if still set) rebuilds on next getSnapshot
|
||||
this.notifyPending = false
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.rebuild()
|
||||
}
|
||||
for (const listener of this.listeners) listener()
|
||||
})
|
||||
if (this.scheduled === 'microtask') return
|
||||
this.schedule('microtask')
|
||||
}
|
||||
|
||||
/** Stream-change entry: mark dirty and publish the cumulative state at most once per frame. */
|
||||
markFrameDirty(): void {
|
||||
this.dirty = true
|
||||
this.notifyPending = true
|
||||
if (this.scheduled !== 'none') return
|
||||
this.schedule(typeof globalThis.requestAnimationFrame === 'function' ? 'frame' : 'microtask')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +56,8 @@ export class Notifier {
|
||||
notifyNow(): void {
|
||||
this.dirty = true
|
||||
this.notifyPending = true
|
||||
if (this.listeners.size === 0) return // lazy: same as markDirty, next getSnapshot rebuilds
|
||||
this.notifyPending = false
|
||||
this.dirty = false
|
||||
this.rebuild()
|
||||
for (const listener of this.listeners) listener()
|
||||
this.invalidateSchedule()
|
||||
this.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,4 +69,35 @@ export class Notifier {
|
||||
this.dirty = false
|
||||
this.rebuild()
|
||||
}
|
||||
|
||||
private schedule(kind: 'microtask' | 'frame'): void {
|
||||
const generation = ++this.scheduleGeneration
|
||||
this.scheduled = kind
|
||||
const publish = () => {
|
||||
if (generation !== this.scheduleGeneration) return
|
||||
this.scheduled = 'none'
|
||||
this.flush()
|
||||
}
|
||||
if (kind === 'frame') {
|
||||
globalThis.requestAnimationFrame(publish)
|
||||
} else {
|
||||
queueMicrotask(publish)
|
||||
}
|
||||
}
|
||||
|
||||
private invalidateSchedule(): void {
|
||||
this.scheduleGeneration++
|
||||
this.scheduled = 'none'
|
||||
}
|
||||
|
||||
private flush(): void {
|
||||
if (!this.notifyPending) return
|
||||
if (this.listeners.size === 0) return // lazy: dirty (if still set) rebuilds on next getSnapshot
|
||||
this.notifyPending = false
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.rebuild()
|
||||
}
|
||||
for (const listener of this.listeners) listener()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,19 @@ import type { StreamChunk } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { AssistantBlock, PartialAssistant } from './conversation.ts'
|
||||
import { toAssistantBlock } from './conversation.ts'
|
||||
|
||||
/**
|
||||
* Whether a stream chunk changes the partial assistant projection shown by the UI.
|
||||
* @param type - Stream chunk discriminant.
|
||||
* @returns Whether publishing the accumulated partial can change the visible snapshot.
|
||||
*/
|
||||
export function isVisibleAssistantChunk(type: string): boolean {
|
||||
return type === 'block-start'
|
||||
|| type === 'text-delta'
|
||||
|| type === 'reasoning-delta'
|
||||
|| type === 'tool-call-delta'
|
||||
|| type === 'block-end'
|
||||
}
|
||||
|
||||
/** assistant/chunk accumulator: folds StreamChunks into AssistantBlock[] with block-level immutability. */
|
||||
export class PartialAccumulator {
|
||||
// Sparse on purpose: block-start may arrive out of order, leaving holes until compaction.
|
||||
|
||||
@@ -21,7 +21,7 @@ import { PendingWait } from './pending.ts'
|
||||
import { TranscriptAdapter } from './transcript-adapter.ts'
|
||||
import { displayFailureMessage } from './failure-display.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
import { PartialAccumulator } from './partial.ts'
|
||||
import { isVisibleAssistantChunk, PartialAccumulator } from './partial.ts'
|
||||
import { ProjectionValueStore } from './projection-store.ts'
|
||||
import type { ProjectionsBaseline } from './projection-store.ts'
|
||||
|
||||
@@ -690,6 +690,10 @@ export class Session implements SessionFace {
|
||||
return
|
||||
}
|
||||
this.appendLive(event, view)
|
||||
if (event.type === 'assistant/chunk') {
|
||||
if (isVisibleAssistantChunk(event.data.chunk.type)) this.notifier.markFrameDirty()
|
||||
return
|
||||
}
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user