import type {ExternalSource} from './selectors.ts'; /** Presentation invalidations only. Commands, history and network work never wait here. */ export type FrameClock={request(callback:()=>void):unknown;cancel(handle:unknown):void}; export function createFrameQueue(clock:FrameClock,publish:(dirty:ReadonlySet)=>void){ let dirty=new Set(),scheduled:{handle?:unknown}|undefined,disposed=false; function flush(){if(disposed)return;const pending=scheduled;scheduled=undefined;if(pending)clock.cancel(pending.handle);if(!dirty.size)return;const batch=dirty;dirty=new Set();publish(batch);} return Object.freeze({ invalidate(keys:Iterable){if(disposed)return;for(const key of keys)dirty.add(key);if(!dirty.size||scheduled)return;const pending: {handle?:unknown}={};scheduled=pending;try{pending.handle=clock.request(()=>{if(scheduled!==pending)return;scheduled=undefined;flush();});}catch(error){scheduled=undefined;throw error;}}, flush, dispose(){if(disposed)return;disposed=true;if(scheduled)clock.cancel(scheduled.handle);scheduled=undefined;dirty.clear();}, }); } /** Coalesce presentation notifications; immediate invalidations flush pending work synchronously. */ export function createFrameSource(source:{getSnapshot():S;subscribe(notify:(immediate?:boolean)=>void):()=>void},clock:FrameClock):ExternalSource{ return Object.freeze({getSnapshot:()=>source.getSnapshot(),subscribe(notify:()=>void){ const queue=createFrameQueue(clock,()=>notify()); let stop:()=>void; try{stop=source.subscribe(immediate=>{queue.invalidate([0]);if(immediate)queue.flush();});}catch(error){queue.dispose();throw error;} return()=>{try{stop();}finally{queue.dispose();}}; }}); }