81 lines
7.1 KiB
TypeScript
81 lines
7.1 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. */
|
||
/** 建立按视图模式和几何手势分离的通道;只追踪 modes/gesture,不把 selection/viewport/connectionGesture 混入。
|
||
* modes 是 viewId→elementId→观察器桶,gestures 是 elementId→观察器桶,同一上游变化先选候选再比值。
|
||
* 有观察器时 previous 持有一份上游快照引用供比较,不复制完整 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;
|
||
/** 按 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<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;
|
||
}
|
||
/** 每个 channel 独享 getSnapshot 的派生值缓存;每个订阅另存 observed,二者不靠回调共享可变业务数据。
|
||
* equal 深比较派生 JSON 风格值,等价时保留旧引用;选中 preview 仍借用原不可变 Geometry,不做深克隆。
|
||
* observer.read 失败会标记失败并通知,后续成功也通知一次恢复;直接 getSnapshot 的读取异常仍向外抛出。 */
|
||
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;}}
|
||
// 退订 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<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);};
|
||
});},
|
||
/** 入口捕获 target.id/incarnation,之后改调用者 target 不影响本通道;订阅按 ID 分桶,读值再匹配代次。
|
||
* 匹配元素跨视图可见 preview,只有 gesture.viewId 对应视图 active;没有 preview 不代表不在手势 targets 中。 */
|
||
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);};});},
|
||
});
|
||
}
|