Expose React canvas hooks with scoped selectors and resource lifecycles
This commit is contained in:
@@ -75,6 +75,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
const digestLifecycle=new AbortController();
|
||||
const sourceDigest=(bytes:Blob,signal?:AbortSignal)=>{const combined=signal?AbortSignal.any([signal,digestLifecycle.signal]):digestLifecycle.signal;return options.digests?options.digests.digest(bytes,combined):importSourceDigest(bytes,combined);};
|
||||
const report = (error: unknown) => { try { options.onError?.(error); } catch { /* Error reporting cannot alter state. */ } };
|
||||
const durabilityListeners=new Set<()=>void>();let durabilityError:string|undefined,durabilityCache:Readonly<{rootRevision:number;durableRootRevision:number;closing:boolean;closed:boolean;error?:string}>|undefined;
|
||||
function getDurability(){const rootRevision=editor.getSnapshot().rootRevision;if(!durabilityCache||durabilityCache.rootRevision!==rootRevision||durabilityCache.durableRootRevision!==durableRootRevision||durabilityCache.closing!==closing||durabilityCache.closed!==closed||durabilityCache.error!==durabilityError)durabilityCache=Object.freeze({rootRevision,durableRootRevision,closing,closed,...(durabilityError?{error:durabilityError}:{})});return durabilityCache;}
|
||||
function notifyDurability(){for(const listener of [...durabilityListeners])try{listener();}catch(error){report(error);}}
|
||||
const createId = options.createId ?? (() => crypto.randomUUID());
|
||||
const network = createNetworkGate(2);
|
||||
const depot=options.assetDepot??(options.storage.assetDepot?createAssetDepot({scope:options.scope,port:options.storage.assetDepot,current:()=>!closing&&!closed&&(!options.isCurrentIdentity||options.isCurrentIdentity()),digest:sourceDigest}):undefined);
|
||||
@@ -195,15 +198,15 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
const depot=options.assets?.authority&&options.storage.assetDepot?await prepareDepotCut({snapshot,registry:options.registry,storage:options.storage,leader,authority:options.assets.authority.scope,files:fileWrites,adoptedFiles,displays:displayPins,temporary:admissionPins(snapshot)}):undefined;
|
||||
const receipt = await options.storage.commit(leader, { image: encodeRecovery(snapshot), files,deleteFiles,...(depot?{depot}:{}) });
|
||||
if (receipt.durableRootRevision !== revision) throw fault('invalid_durable_receipt');
|
||||
durableRootRevision = revision; pending.delete(revision);
|
||||
durableRootRevision = revision; pending.delete(revision);notifyDurability();
|
||||
for(const id of deleteFiles){durablyPurged.add(id);collectionReservations.delete(id);}
|
||||
for (const file of files) fileWrites.delete(file.id);
|
||||
for(const [id,entry]of adoptedFiles)if(entry.rootRevision<=snapshot.rootRevision&&depot?.writes.some(write=>write.ref.asset.id===id&&write.bytes===entry.bytes))adoptedFiles.delete(id);
|
||||
if (editor.getSnapshot().runtime.ledger.durableGeneration < snapshot.runtime.ledger.generation) mutate(ledger => ledger.markDurable(snapshot.runtime.ledger.generation));
|
||||
}
|
||||
persistenceFailed = false;
|
||||
persistenceFailed = false;durabilityError=undefined;notifyDurability();
|
||||
})();
|
||||
writing = task.catch(error => { persistenceFailed = true; throw error; }).finally(() => { writing = undefined;if(!persistenceFailed){if(loadingCompletionRevision!==undefined&&durableRootRevision>=loadingCompletionRevision&&!editor.getSnapshot().runtime.loading){const release=loadingRelease;loadingRelease=undefined;loadingCompletionRevision=undefined;release?.();}if(!closing&&!closed)scheduler.refreshAdmission();} });
|
||||
writing = task.catch(error => { persistenceFailed = true;const failure=error&&typeof error==='object'?error as {code?:unknown;message?:unknown}:{};durabilityError=typeof failure.code==='string'&&failure.code?failure.code:typeof failure.message==='string'&&failure.message?failure.message:'persistence_failed';notifyDurability(); throw error; }).finally(() => { writing = undefined;if(!persistenceFailed){if(loadingCompletionRevision!==undefined&&durableRootRevision>=loadingCompletionRevision&&!editor.getSnapshot().runtime.loading){const release=loadingRelease;loadingRelease=undefined;loadingCompletionRevision=undefined;release?.();}if(!closing&&!closed)scheduler.refreshAdmission();} });
|
||||
return writing;
|
||||
}
|
||||
function collectResources(settings:{retentionMs:number;maxFiles?:number}) {
|
||||
@@ -518,7 +521,7 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
const sessions = createSessions(editor);
|
||||
const unsubscribeSizing = sessions.subscribe(() => { if (!sessions.getSnapshot().gesture) queueDefaultSizes(); });
|
||||
const unsubscribe = editor.subscribe((commit, snapshot) => {
|
||||
pending.set(snapshot.rootRevision, snapshot);
|
||||
pending.set(snapshot.rootRevision, snapshot);notifyDurability();
|
||||
const addedRuns=[...snapshot.runtime.runs.keys()].filter(id=>!trackedRuns.has(id));for(const id of addedRuns)trackedRuns.add(id);
|
||||
if(addedRuns.length)queueMicrotask(()=>runTracker.notify(addedRuns));
|
||||
if (!scheduled) { scheduled = true; queueMicrotask(() => { scheduled = false; void persist().catch(report); }); }
|
||||
@@ -980,17 +983,18 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
},
|
||||
getSnapshot: editor.getSnapshot, persist,
|
||||
cancelRecovery:async()=>{await recovery?.cancel();},
|
||||
getDurability: () => Object.freeze({ rootRevision: editor.getSnapshot().rootRevision, durableRootRevision }),
|
||||
getDurability,
|
||||
subscribeDurability(listener:()=>void){if(closed)return()=>{};durabilityListeners.add(listener);return()=>{durabilityListeners.delete(listener);};},
|
||||
retry: async () => { if(recovery)await recovery.cancel();assertWritable(false);await persist();retryDefaultSizes();const runtime=editor.getSnapshot().runtime,ids=[...runtime.resources.values()].filter(resource=>!resource.digest&&runtime.ledger.operations.get(resource.operationId)?.blockers.some(code=>digestFailures.has(code))).map(resource=>resource.operationId);if(ids.length){mutate(ledger=>{for(const id of ids)ledger.block(id,['waiting_digest']);});prepareDigests();}if(options.images){editor.updateRuntime(previous=>{const resources=new Map(previous.resources);let changed=false;for(const [id,resource]of resources)if(resource.image?.version===1&&resource.image.status==='failed'){resources.set(id,freeze({...resource,image:{version:1 as const,status:'pending' as const}}));changed=true;}return changed?Object.freeze({...previous,resources:readMap(resources)}):previous;});prepareImages();}await scheduler.resume();await runTracker.retry(); },
|
||||
async settle() { await Promise.resolve();await Promise.allSettled([...adoptionTasks.values()]);await Promise.allSettled([...copyPreparations.values()]);while(importLoads.size)await Promise.all([...importLoads.values()]);await Promise.all([...importRunners.values()].map(runner=>runner.settle())); while (digestTasks.size) await Promise.all([...digestTasks.values()]);while(imageTasks.size)await Promise.all([...imageTasks.values()]);while(sizingTask)await sizingTask; await scheduler.settle(); await runTracker.settle(); await persist();await scheduler.settle();await persist(); },
|
||||
async close() {
|
||||
if (closed) return;
|
||||
closing = true;loadingReadController?.abort();await loadingRead?.catch(()=>{});unsubscribeSizing();orphanController?.abort();await Promise.allSettled([collection,sourceCollection,orphanCollection].filter(Boolean));digestLifecycle.abort();for(const controller of preparationControllers.values())controller.abort();for(const controller of copyPreparations.keys())controller.abort();await Promise.allSettled([...copyPreparations.values()]);for(const controller of adoptionTasks.keys())controller.abort();await Promise.allSettled([...adoptionTasks.values()]); sessions.dispose(); scheduler.stop(); runTracker.stop();
|
||||
closing = true;notifyDurability();loadingReadController?.abort();await loadingRead?.catch(()=>{});unsubscribeSizing();orphanController?.abort();await Promise.allSettled([collection,sourceCollection,orphanCollection].filter(Boolean));digestLifecycle.abort();for(const controller of preparationControllers.values())controller.abort();for(const controller of copyPreparations.keys())controller.abort();await Promise.allSettled([...copyPreparations.values()]);for(const controller of adoptionTasks.keys())controller.abort();await Promise.allSettled([...adoptionTasks.values()]); sessions.dispose(); scheduler.stop(); runTracker.stop();
|
||||
await Promise.all([...importRunners.values()].map(runner=>runner.close()));while(importLoads.size)await Promise.all([...importLoads.values()]);while(digestTasks.size)await Promise.allSettled([...digestTasks.values()]);while(imageTasks.size)await Promise.allSettled([...imageTasks.values()]);while(sizingTask)await sizingTask;
|
||||
recovery?.controller.abort();recovery?.release();
|
||||
for (const release of [...displays]) release();displayCache.releaseOwner(displayOwner);if(!options.displays)displayCache.close();
|
||||
// Failure keeps the leader/recovery material available for an explicit retry of close.
|
||||
await persist(); unsubscribe(); editor.dispose();if(!options.assetDepot)depot?.close();adoptedFiles.clear(); await leader.release(); closed = true;
|
||||
await persist(); unsubscribe(); editor.dispose();if(!options.assetDepot)depot?.close();adoptedFiles.clear(); await leader.release(); closed = true;notifyDurability();durabilityListeners.clear();
|
||||
},
|
||||
});
|
||||
} catch (error) { await leader.release(); throw error; }
|
||||
|
||||
Reference in New Issue
Block a user