Coalesce session presentation invalidations through a disposable frame queue
This commit is contained in:
@@ -10,6 +10,7 @@ import { defaultCanvasRenderers } from "./canvas-renderers";
|
||||
import type { Data, ElementRecord, Geometry, Id, Json, Result } from "@/lib/canvas-core/types";
|
||||
import { equal } from "@/lib/canvas-core/values";
|
||||
import {createGeometryProjectionCache} from "@/lib/canvas-bindings/geometry-cache";
|
||||
import {createSessionFrameSource} from "@/lib/canvas-bindings/session-frames";
|
||||
import {routeCanvasKey} from "@/lib/canvas-bindings/input-router";
|
||||
import {hitProjectedCanvas} from "@/lib/canvas-bindings/hit-query";
|
||||
import {compatConnector,measuredConnector} from "@/lib/canvas-bindings/connectors";
|
||||
@@ -37,7 +38,8 @@ 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 subscribe = useCallback((notify: () => void) => engine.editor.subscribe(notify), [engine]);
|
||||
const root = useSyncExternalStore(subscribe, engine.getSnapshot, engine.getSnapshot);
|
||||
const sessions = useSyncExternalStore(engine.sessions.subscribe, engine.sessions.getSnapshot, engine.sessions.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);
|
||||
const loading=root.runtime.loading,needsLoadingFit=useRef(!!loading&&fitLoading);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 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){
|
||||
let dirty=new Set<K>(),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<K>){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();},
|
||||
});
|
||||
}
|
||||
@@ -12,3 +12,6 @@ export {createSelectorStore} from './selectors.ts';
|
||||
export type {ExternalSource} from './selectors.ts';
|
||||
export {routeCanvasKey} from './input-router.ts';
|
||||
export type {CanvasKeyInput,InputClaim,KeyHandler} from './input-router.ts';
|
||||
export {createFrameQueue} from './frame-queue.ts';
|
||||
export type {FrameClock} from './frame-queue.ts';
|
||||
export {createSessionFrameSource} from './session-frames.ts';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type {SessionSnapshot} from '../canvas-core/gestures.ts';
|
||||
import type {ExternalSource} from './selectors.ts';
|
||||
import {createFrameQueue,type FrameClock} from './frame-queue.ts';
|
||||
/** One subscription owns one disposable frame queue. Reads always see current sessions. */
|
||||
export function createSessionFrameSource(source:ExternalSource<SessionSnapshot>,clock:FrameClock):ExternalSource<SessionSnapshot>{
|
||||
return Object.freeze({getSnapshot:source.getSnapshot,subscribe(notify:()=>void){
|
||||
let previous=source.getSnapshot();const queue=createFrameQueue<string>(clock,()=>notify());
|
||||
const stop=source.subscribe(()=>{const next=source.getSnapshot(),dirty=new Set<string>();
|
||||
for(const [id,view]of previous.views)if(next.views.get(id)!==view)dirty.add(`view:${id}`);
|
||||
for(const [id,view]of next.views)if(previous.views.get(id)!==view)dirty.add(`view:${id}`);
|
||||
if(previous.gesture!==next.gesture)for(const state of [previous.gesture,next.gesture])for(const target of state?.targets??[])dirty.add(`element:${target.id}`);
|
||||
if(previous.connectionGesture!==next.connectionGesture)for(const state of [previous.connectionGesture,next.connectionGesture])if(state)dirty.add(`element:${state.target.id}`);
|
||||
previous=next;queue.invalidate(dirty);
|
||||
});return()=>{stop();queue.dispose();};
|
||||
}});
|
||||
}
|
||||
Reference in New Issue
Block a user