| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- import { notifySubscribers } from '@deepseek-ai/dsh-client-store'
- /**
- * Batches structural updates in microtasks and stream updates by animation
- * frame. Reads may rebuild a dirty snapshot without consuming the pending
- * subscriber notification.
- */
- export class Notifier {
- private listeners = new Set<() => void>()
- private dirty = false
- private notifyPending = 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) {}
- /**
- * uSES subscription entry.
- * @param listener - change callback.
- * @returns the unsubscribe function.
- */
- subscribe(listener: () => void): () => void {
- this.listeners.add(listener)
- return () => {
- this.listeners.delete(listener)
- }
- }
- /** Mark the snapshot dirty and notify in a microtask. */
- markDirty(): void {
- this.dirty = true
- this.notifyPending = true
- if (this.scheduled === 'microtask') return
- this.schedule('microtask')
- }
- /** Mark the snapshot dirty and publish 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')
- }
- /**
- * Synchronous flush: controlled-input writes must notify in the same tick as
- * onChange, or React rolls the DOM back to the stale value and the caret jumps to the end.
- */
- notifyNow(): void {
- this.dirty = true
- this.notifyPending = true
- this.invalidateSchedule()
- this.flush()
- }
- /**
- * Pre-getSnapshot check: rebuild synchronously when dirty (read path
- * before first subscribe / while unobserved). Notification stays pending.
- */
- ensureFresh(): void {
- if (!this.dirty) return
- 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()
- }
- notifySubscribers(this.listeners, '[session-controller]')
- }
- }
|