Coalesce background Root notifications without delaying input updates

This commit is contained in:
2026-11-10 17:56:00 +08:00
parent 0abb7c96ba
commit b7fe214d97
5 changed files with 56 additions and 5 deletions
@@ -15,6 +15,7 @@ import {nativeRectVisible} from "@/lib/canvas-bindings/visibility";
import {createOverlayIndex} from "@/lib/canvas-bindings/overlays";
import {CanvasOverlays} from "./canvas-overlays";
import {createProjectedPaintIndex} from "@/lib/canvas-bindings/projected-paint";
import {createFrameSource} from '@/lib/canvas-bindings/frame-queue';
import {createSessionFrameSource} from "@/lib/canvas-bindings/session-frames";
import {routeCanvasKey,canvasKeyContext} from "@/lib/canvas-bindings/input-router";
import {hitProjectedCanvas} from "@/lib/canvas-bindings/hit-query";
@@ -42,11 +43,17 @@ import styles from "./canvas.module.css";
const json = (value: unknown): Data => JSON.parse(JSON.stringify(value));
function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engine: ProjectEngine; board: Board; viewId: Id; renderers:ReactRenderers;fitLoading?:boolean }) {
const sceneCache=useMemo(()=>createSceneProjection(),[engine,viewId]);
const subscribe = useCallback((notify: () => void) => {
const subscribe = useCallback((notify: (immediate?:boolean) => void) => {
let before=engine.getSnapshot().document;
return engine.editor.subscribe((commit,next)=>{sceneCache.advance(before,next.document,commit.changes);before=next.document;notify();});
return engine.editor.subscribe((commit,next)=>{sceneCache.advance(before,next.document,commit.changes);before=next.document;
// Controlled inputs must publish their new value before the native event
// finishes. Batch creation/background work may share a presentation frame.
const immediate=commit.origin==='undo'||commit.origin==='redo'||commit.origin==='user'&&commit.changes.some(change=>change.kind==='field'||change.before!==null);
notify(immediate);
});
}, [engine,sceneCache]);
const root = useSyncExternalStore(subscribe, engine.getSnapshot, engine.getSnapshot);
const rootFrames=useMemo(()=>createFrameSource({subscribe,getSnapshot:engine.getSnapshot},{request:callback=>requestAnimationFrame(callback),cancel:handle=>cancelAnimationFrame(handle as number)}),[engine,subscribe]);
const root = useSyncExternalStore(rootFrames.subscribe, rootFrames.getSnapshot, rootFrames.getSnapshot);
const sessionFrames=useMemo(()=>createSessionFrameSource(engine.sessions,{request:callback=>requestAnimationFrame(callback),cancel:handle=>cancelAnimationFrame(handle as number)}),[engine]);
const sessions = useSyncExternalStore(sessionFrames.subscribe, sessionFrames.getSnapshot, sessionFrames.getSnapshot);
const liveView = sessions.views.get(viewId), lastView = useRef(liveView);
+11
View File
@@ -1,3 +1,4 @@
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<K>(clock:FrameClock,publish:(dirty:ReadonlySet<K>)=>void){
@@ -9,3 +10,13 @@ export function createFrameQueue<K>(clock:FrameClock,publish:(dirty:ReadonlySet<
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<S>(source:{getSnapshot():S;subscribe(notify:(immediate?:boolean)=>void):()=>void},clock:FrameClock):ExternalSource<S>{
return Object.freeze({getSnapshot:()=>source.getSnapshot(),subscribe(notify:()=>void){
const queue=createFrameQueue<number>(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();}};
}});
}
+1 -1
View File
@@ -12,7 +12,7 @@ export {createSelectorStore} from './selectors.ts';
export type {ExternalSource} from './selectors.ts';
export {routeCanvasKey,canvasKeyContext} from './input-router.ts';
export type {CanvasKeyInput,InputClaim,KeyHandler} from './input-router.ts';
export {createFrameQueue} from './frame-queue.ts';
export {createFrameQueue,createFrameSource} from './frame-queue.ts';
export type {FrameClock} from './frame-queue.ts';
export {createSessionFrameSource} from './session-frames.ts';
export {createPaintIndex} from './paint-index.ts';