63 lines
4.5 KiB
TypeScript
63 lines
4.5 KiB
TypeScript
/**
|
|
* 迁移协调器的浏览器装配层。旧编辑视图持共享Web Lock,迁移操作取同一键的独占锁,
|
|
* 配合本地pending记录阻止已发出迁移却结果未知时继续旧格式写入。锁协调本浏览器,服务端版本/幂等校验另行负责。
|
|
*/
|
|
import type {Registry} from './canvas-core/types.ts';
|
|
import {fault} from './canvas-core/values.ts';
|
|
import {createFoundryTransport} from './canvas-service/foundry.ts';
|
|
import {canvasMigrationStorageKey,createCanvasMigration,migrationBlocksCanvas,readCanvasMigration,type CanvasMigrationScope} from './canvas-migration.ts';
|
|
|
|
/** 页面提供身份、可迁移根和完成后的重开回调;rootId缺失不代表可任意推测迁移范围。 */
|
|
export type LegacyMigrationContext={accountId:string;rootId?:string;registry:Registry;isCurrentIdentity():boolean;onApplied():void};
|
|
/** 锁/Storage可注入测试,生产默认使用navigator.locks/localStorage;身份和signal都由宿主管理。 */
|
|
type BrowserScope={scope:CanvasMigrationScope;isCurrentIdentity():boolean;signal:AbortSignal;locks?:LockManager;storage?:Storage};
|
|
/** 同账户/工作区共享编辑锁键,与持久迁移记录同一作用域,跨页面形成读写互斥。 */
|
|
const lockKey=(scope:CanvasMigrationScope)=>canvasMigrationStorageKey(scope)+':editing';
|
|
/** 缺少Web Locks明确拒绝安全迁移装配,不用不可靠的计时器冒充独占锁。 */
|
|
function ports(options:BrowserScope){
|
|
const locks=options.locks??globalThis.navigator?.locks;
|
|
if(!locks)throw fault('migration_lock_unavailable');
|
|
return{locks,storage:options.storage??globalThis.localStorage};
|
|
}
|
|
/** 获取锁前后均检查身份/取消,旧页面不能借已等待的锁继续对新账户操作。 */
|
|
const check=(options:BrowserScope)=>{if(!options.isCurrentIdentity())throw fault('identity_changed');options.signal.throwIfAborted();};
|
|
|
|
/** Held for the entire lifetime of an editable legacy view, not just each PATCH. */
|
|
/** 为旧编辑视图整个生命周期借共享锁,release返回Promise确认锁请求已结束。
|
|
* ifAvailable不排长队;迁移占用或本canvas有未确认迁移时立即拒绝开放旧编辑。 */
|
|
export async function claimLegacyCanvasEditing(options:BrowserScope&{canvasId:string}){
|
|
check(options);const{locks,storage}=ports(options);
|
|
let resolve!:(value:{release():Promise<void>})=>void,reject!:(error:unknown)=>void;
|
|
// ready在锁授予且本地pending检查通过后返回;request则在release/abort后才结束,两者用途不同。
|
|
const ready=new Promise<{release():Promise<void>}>((yes,no)=>{resolve=yes;reject=no;});
|
|
const request=locks.request(lockKey(options.scope),{mode:'shared',ifAvailable:true},async lock=>{
|
|
try{
|
|
check(options);if(!lock)throw fault('migration_busy');
|
|
if(migrationBlocksCanvas(readCanvasMigration(storage,options.scope),options.canvasId))throw fault('migration_response_unknown');
|
|
check(options);
|
|
// 回调等待released以保持共享锁;不能在第一次PATCH完成后就把编辑门解除。
|
|
let unlock!:()=>void;const released=new Promise<void>(yes=>{unlock=yes;});
|
|
/** 解开锁回调并等待平台释放;resolve可重复调用,已释放后的再次release仍等同一请求结束。 */
|
|
const release=async()=>{unlock();await request;};
|
|
/** 页面生命周期中止时释放锁,异步清理异常不制造未处理拒绝。 */
|
|
const abort=()=>{void release().catch(()=>{});};options.signal.addEventListener('abort',abort,{once:true});
|
|
resolve({release});
|
|
try{await released;}finally{options.signal.removeEventListener('abort',abort);}
|
|
}catch(error){reject(error);}
|
|
});
|
|
void request.catch(reject);return ready;
|
|
}
|
|
|
|
/** 连接localStorage、专属账户transport和UUID生成器;真正迁移在同一工作区独占锁内执行。 */
|
|
export function createBrowserCanvasMigration(options:BrowserScope&{registry:Registry}){
|
|
check(options);const{locks,storage}=ports(options);
|
|
return createCanvasMigration({scope:options.scope,registry:options.registry,storage,
|
|
transport:createFoundryTransport({accountId:options.scope.accountId,isCurrentIdentity:options.isCurrentIdentity}),
|
|
isCurrentIdentity:options.isCurrentIdentity,signal:options.signal,createId:()=>crypto.randomUUID(),
|
|
// 任一共享旧编辑页仍开着时独占锁不可用;调用方先保存/释放自己的编辑租约,再进入迁移。
|
|
lock:work=>locks.request(lockKey(options.scope),{mode:'exclusive',ifAvailable:true},async lock=>{
|
|
check(options);if(!lock)throw fault('migration_busy');return work();
|
|
}),
|
|
});
|
|
}
|