feat(canvas): gate legacy editing and hand off explicit migration safely
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
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';
|
||||
|
||||
export type LegacyMigrationContext={accountId:string;rootId?:string;registry:Registry;isCurrentIdentity():boolean;onApplied():void};
|
||||
type BrowserScope={scope:CanvasMigrationScope;isCurrentIdentity():boolean;signal:AbortSignal;locks?:LockManager;storage?:Storage};
|
||||
const lockKey=(scope:CanvasMigrationScope)=>canvasMigrationStorageKey(scope)+':editing';
|
||||
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. */
|
||||
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;
|
||||
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);
|
||||
let unlock!:()=>void;const released=new Promise<void>(yes=>{unlock=yes;});
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user