Files
Endoleap/src/lib/canvas-bindings/session-sources.ts
T

72 lines
5.9 KiB
TypeScript

/**
* 按 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. */
/** 建立按视图模式和元素手势分离的订阅通道,比较派生值后才通知观察者。 */
export function createSessionSources(source:ExternalSource<SessionSnapshot>){
// update 先推进内部观察值,notify 后执行外部回调,避免某监听器读到其它桶尚未更新。
type Observer={update(snapshot:SessionSnapshot):boolean;notify():void};
const modes=new Map<Id,Map<Id,Set<Observer>>>(),gestures=new Map<Id,Set<Observer>>();
// 按不可变 targets 数组的身份弱缓存 ID→代次索引,避免每个元素订阅逐次线性搜索目标列表。
const targetIndexes=new WeakMap<readonly Target[],ReadonlyMap<Id,number>>();
let stop:(()=>void)|undefined,previous:SessionSnapshot|undefined;
/** 首次遇到一组目标时建索引;相同 ID 的新 incarnation 不会匹配旧手势。 */
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;}
/** 收集可能受影响的观察器,全部 update 完再逐个 notify;一个监听器失败也不饿死其余监听器。 */
function changed(){
if(!previous)return;const before=previous,next=source.getSnapshot();previous=next;
const candidates=new Set<Observer>(),add=(items:Set<Observer>|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<Id>();
// 手势身份/视图/目标集合变化要刷新全组;同一手势只有 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;
}
/** 一个派生通道的稳定读取和订阅装配。读取失败通过通知触发使用方重读,不把错误吞成旧成功值。 */
function channel<T>(read:(snapshot:SessionSnapshot)=>T,register:(observer:Observer)=>()=>void):ExternalSource<T>{
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;}}
// 单次退订幂等;所有桶清空后释放全局上游订阅及上一份会话快照。
let active=true;return()=>{if(!active)return;active=false;remove();if(!modes.size&&!gestures.size){stop?.();stop=undefined;previous=undefined;}};
},
});
}
return Object.freeze({
/** 派生某视图中单元素的编辑模式;视图/模式缺失时返回 null。 */
mode(viewId:Id,id:Id):ExternalSource<string|null>{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);};
});},
/** 按目标 ID+代次读取预览;同元素跨视图可见预览,但只有原 viewId 拥有 active。 */
gesture(viewId:Id,target:Target):ExternalSource<GestureSelection>{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);};});},
});
}