Keep unrelated renderers idle through local edits and acknowledgments

This commit is contained in:
2026-11-09 19:09:12 +08:00
parent 43c74a1485
commit 28c792d640
7 changed files with 35 additions and 12 deletions
+4 -1
View File
@@ -14,7 +14,10 @@ export function createProjectionCache(){
const next=new Map<Id,Retained<T>>();
for(let index=0;index<values.length;index++){
const value=values[index],record=document.elements.get(value.id);if(!record)throw fault('projection_identity_missing',[value.id]);
const old=previous.get(value.id),retained=old&&old.incarnation===record.incarnation&&old.ownerCanvasId===record.ownerCanvasId&&equal(old.value,value)?old.value:value;
const old=previous.get(value.id),sameLifetime=old&&old.incarnation===record.incarnation&&old.ownerCanvasId===record.ownerCanvasId;
let shared=value;
if(sameLifetime)for(const key of ['data','style','position'] as const){const incoming=(shared as Record<string,unknown>)[key],before=(old.value as Record<string,unknown>)[key];if(Object.hasOwn(shared,key)&&Object.hasOwn(old.value,key)&&!Object.is(incoming,before)&&equal(incoming,before))shared={...shared,[key]:before};}
const retained=sameLifetime&&equal(old.value,shared)?old.value:shared;
values[index]=retained;next.set(value.id,{incarnation:record.incarnation,ownerCanvasId:record.ownerCanvasId,value:retained});
}
return next;
+10 -3
View File
@@ -1,3 +1,4 @@
import {memo} from 'react';
import type { NodeTypes, EdgeTypes } from '@xyflow/react';
import type { ComponentDefinition, Registry } from '../canvas-core/types.ts';
import { fault } from '../canvas-core/values.ts';
@@ -5,6 +6,12 @@ import { createRendererCatalog, type RendererDescriptor, type RendererCatalog }
export type ReactRendererDefinition=RendererDescriptor&({readonly structure:'node';readonly render:NodeTypes[string];readonly preview?:NodeTypes[string]}|{readonly structure:'connection';readonly render:EdgeTypes[string];readonly preview?:EdgeTypes[string]});
export type ReactRenderers={readonly catalog:RendererCatalog;readonly definitions:readonly ReactRendererDefinition[];readonly nodeTypes:NodeTypes;readonly edgeTypes:EdgeTypes;readonly previewNodeTypes:NodeTypes;readonly previewEdgeTypes:EdgeTypes};
export type CanvasRuntimeExtension={readonly components?:readonly ComponentDefinition[];readonly renderers?:readonly ReactRendererDefinition[]};
const memoizedRenderers=new WeakMap<object,NodeTypes[string]|EdgeTypes[string]>();
function compiledRenderer(render:NodeTypes[string]|EdgeTypes[string]):NodeTypes[string]|EdgeTypes[string]{
if((render as {$$typeof?:symbol}).$$typeof===Symbol.for('react.memo'))return render;
const key=render as object,previous=memoizedRenderers.get(key);if(previous)return previous;
const compiled=memo(render as NodeTypes[string]);memoizedRenderers.set(key,compiled);return compiled;
}
export function createReactRenderers(definitions:readonly ReactRendererDefinition[],registry?:Registry):ReactRenderers{
const catalog=createRendererCatalog(definitions),nodes:NodeTypes=Object.create(null),edges:EdgeTypes=Object.create(null),previewNodes:NodeTypes=Object.create(null),previewEdges:EdgeTypes=Object.create(null),captured:ReactRendererDefinition[]=[];
for(let index=0;index<definitions.length;index++){
@@ -12,9 +19,9 @@ export function createReactRenderers(definitions:readonly ReactRendererDefinitio
if(typeof render!=='function'&&![Symbol.for('react.memo'),Symbol.for('react.forward_ref'),Symbol.for('react.lazy')].includes(tag as symbol))throw fault('invalid_react_renderer');
const previewTag=preview&&typeof preview==='object'?(preview as {$$typeof?:symbol}).$$typeof:undefined;if(preview!==undefined&&typeof preview!=='function'&&![Symbol.for('react.memo'),Symbol.for('react.forward_ref'),Symbol.for('react.lazy')].includes(previewTag as symbol))throw fault('invalid_react_preview');
if(registry){const model=registry.components.get(descriptor.componentId);if(!model||model.dataVersion!==descriptor.dataVersion||model.structure!==descriptor.structure)throw fault('renderer_model_mismatch',[descriptor.componentId]);}
const table=descriptor.structure==='node'?nodes:edges,previews=descriptor.structure==='node'?previewNodes:previewEdges;
if(Object.hasOwn(table,descriptor.rendererId)&&(table[descriptor.rendererId]!==render||previews[descriptor.rendererId]!==preview))throw fault('renderer_id_conflict',[descriptor.rendererId]);
Object.defineProperty(table,descriptor.rendererId,{value:render,enumerable:true,configurable:true});if(preview)Object.defineProperty(previews,descriptor.rendererId,{value:preview,enumerable:true,configurable:true});captured.push(Object.freeze({...descriptor,render,...(preview?{preview}:{})}) as ReactRendererDefinition);
const table=descriptor.structure==='node'?nodes:edges,previews=descriptor.structure==='node'?previewNodes:previewEdges,compiled=compiledRenderer(render),compiledPreview=preview?compiledRenderer(preview):undefined;
if(Object.hasOwn(table,descriptor.rendererId)&&(table[descriptor.rendererId]!==compiled||previews[descriptor.rendererId]!==compiledPreview))throw fault('renderer_id_conflict',[descriptor.rendererId]);
Object.defineProperty(table,descriptor.rendererId,{value:compiled,enumerable:true,configurable:true});if(compiledPreview)Object.defineProperty(previews,descriptor.rendererId,{value:compiledPreview,enumerable:true,configurable:true});captured.push(Object.freeze({...descriptor,render,...(preview?{preview}:{})}) as ReactRendererDefinition);
}
return Object.freeze({catalog,definitions:Object.freeze(captured),nodeTypes:Object.freeze(nodes),edgeTypes:Object.freeze(edges),previewNodeTypes:Object.freeze(previewNodes),previewEdgeTypes:Object.freeze(previewEdges)});
}