/** * 按 view/element 分桶的交互会话订阅。多个元素共享一个上游订阅,变化只路由到相关桶。 * 派生源保留选中值与观察器,不保存第二份完整 session;最后一个观察器离开后退订上游。 */ import type {Id,Target,Geometry} from '../canvas-core/types.ts'; import type {SessionSnapshot,GestureState} from '../canvas-core/gestures.ts'; import {equal} from '../canvas-core/values.ts'; import type {ExternalSource} from './selectors.ts'; /** preview 可被所有匹配代次的视图读取,active 则只标记拥有该手势的视图。 */ export type GestureSelection={readonly preview:Geometry|undefined;readonly active:boolean}; /** Derived view/target subscriptions. Values retain no document or full per-hook session. */ /** 建立按视图模式和几何手势分离的通道;只追踪 modes/gesture,不把 selection/viewport/connectionGesture 混入。 * modes 是 viewId→elementId→观察器桶,gestures 是 elementId→观察器桶,同一上游变化先选候选再比值。 * 有观察器时 previous 持有一份上游快照引用供比较,不复制完整 session;最后退订后释放该引用。 */ export function createSessionSources(source:ExternalSource){ // update 先推进内部观察值,notify 后执行外部回调,避免某监听器读到其它桶尚未更新。 type Observer={update(snapshot:SessionSnapshot):boolean;notify():void}; const modes=new Map>>(),gestures=new Map>(); // 按不可变 targets 数组的身份弱缓存 ID→代次索引,避免每个元素订阅逐次线性搜索目标列表。 const targetIndexes=new WeakMap>(); let stop:(()=>void)|undefined,previous:SessionSnapshot|undefined; /** 按 targets 数组身份缓存 elementId→incarnation,要求上游不原地改 targets。 * 合法会话应保证同组 ID 唯一;Map 对重复 ID 留最后一项,此 helper 不另验目标表形状。 */ function targets(gesture:GestureState){let index=targetIndexes.get(gesture.targets);if(!index){index=new Map(gesture.targets.map(target=>[target.id,target.incarnation]));targetIndexes.set(gesture.targets,index);}return index;} /** 在 changed 入口取得同一份 next,先推进 previous,再用 Set 合并所有候选,避免观察器重复更新。 * 先更新全部候选 observed 后 notify;各 notify 异常暂存,余者仍执行,最后重抛第一个异常(包括假值)。 */ function changed(){ if(!previous)return;const before=previous,next=source.getSnapshot();previous=next; const candidates=new Set(),add=(items:Set|undefined)=>{for(const item of items??[])candidates.add(item);}; // 只检查实际有订阅的视图;模式 Map 或对应元素模式未变时不通知。 if(before.views!==next.views)for(const[viewId,byId]of modes){ const old=before.views.get(viewId)?.modes,current=next.views.get(viewId)?.modes;if(old===current)continue; for(const id of new Set([...(old?.keys()??[]),...(current?.keys()??[])]))if(old?.get(id)!==current?.get(id))add(byId.get(id)); } const old=before.gesture,current=next.gesture; if(old!==current){ const ids=new Set(); // 手势身份/视图/目标集合变化要刷新全组;同一手势只有 preview 变时只刷新预览变化的元素。 if(!old||!current||old.token!==current.token||old.viewId!==current.viewId||old.targets!==current.targets){for(const gesture of [old,current])for(const target of gesture?.targets??[])ids.add(target.id);} else if(old.preview!==current.preview){for(const[id,value]of old.preview)if(current.preview.get(id)!==value)ids.add(id);for(const[id,value]of current.preview)if(old.preview.get(id)!==value)ids.add(id);} for(const id of ids)add(gestures.get(id)); } const notifications:Observer[]=[];for(const observer of candidates)if(observer.update(next))notifications.push(observer); let failed=false,error:unknown;for(const observer of notifications)try{observer.notify();}catch(caught){if(!failed){failed=true;error=caught;}}if(failed)throw error; } /** 每个 channel 独享 getSnapshot 的派生值缓存;每个订阅另存 observed,二者不靠回调共享可变业务数据。 * equal 深比较派生 JSON 风格值,等价时保留旧引用;选中 preview 仍借用原不可变 Geometry,不做深克隆。 * observer.read 失败会标记失败并通知,后续成功也通知一次恢复;直接 getSnapshot 的读取异常仍向外抛出。 */ function channel(read:(snapshot:SessionSnapshot)=>T,register:(observer:Observer)=>()=>void):ExternalSource{ let initialized=false,value:T; return Object.freeze({ getSnapshot(){const next=read(source.getSnapshot());if(!initialized||!equal(value,next))value=next;initialized=true;return value;}, subscribe(notify:()=>void){ let observed=read(source.getSnapshot()),failed=false; const observer:Observer={notify,update(snapshot){try{const next=read(snapshot);if(!failed&&equal(observed,next))return false;observed=next;failed=false;}catch{failed=true;}return true;}}; const remove=register(observer); // 第一个观察器才连上游;注册失败撤回本次桶,防止留下无人管理的观察器。 if(!stop){previous=source.getSnapshot();try{stop=source.subscribe(changed);}catch(error){remove();previous=undefined;throw error;}} // 退订 token 先置 inactive,remove 清桶;最后一桶消失才 stop 上游并清 previous。 // stop 异常不在这里捕获,状态重置不保证继续执行;正常宿主清理端口须不抛错。 let active=true;return()=>{if(!active)return;active=false;remove();if(!modes.size&&!gestures.size){stop?.();stop=undefined;previous=undefined;}}; }, }); } return Object.freeze({ /** 捕获 viewId/id 两个标量,按 view→element 桶登记观察器;视图/模式缺失返回 null。 * 调用 mode 只创建派生通道,首次 subscribe 才注册桶和必要的上游监听。 */ mode(viewId:Id,id:Id):ExternalSource{return channel(snapshot=>snapshot.views.get(viewId)?.modes.get(id)??null,observer=>{ let byId=modes.get(viewId);if(!byId){byId=new Map();modes.set(viewId,byId);}let bucket=byId.get(id);if(!bucket){bucket=new Set();byId.set(id,bucket);}bucket.add(observer); return()=>{bucket!.delete(observer);if(!bucket!.size)byId!.delete(id);if(!byId!.size)modes.delete(viewId);}; });}, /** 入口捕获 target.id/incarnation,之后改调用者 target 不影响本通道;订阅按 ID 分桶,读值再匹配代次。 * 匹配元素跨视图可见 preview,只有 gesture.viewId 对应视图 active;没有 preview 不代表不在手势 targets 中。 */ gesture(viewId:Id,target:Target):ExternalSource{const{id,incarnation}=target;return channel(snapshot=>{ const gesture=snapshot.gesture,matched=!!gesture&&targets(gesture).get(id)===incarnation; return{preview:matched?gesture!.preview.get(id):undefined,active:matched&&gesture!.viewId===viewId}; },observer=>{let bucket=gestures.get(id);if(!bucket){bucket=new Set();gestures.set(id,bucket);}bucket.add(observer);return()=>{bucket!.delete(observer);if(!bucket!.size)gestures.delete(id);};});}, }); }