fix(replay): recover failed archives and release deleted sessions

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 01:55:23 +03:00
parent 2a97cf28c0
commit 1eba7c82ba
8 changed files with 217 additions and 7 deletions
+24
View File
@@ -199,6 +199,7 @@ export default function App() {
const livePerceptionLayersRef = useRef<LivePerceptionLayers>(defaultLivePerceptionLayers);
const livePerceptionRevisionRef = useRef(0);
const replayActiveRef = useRef(false);
const selectedRecordedSessionIdRef = useRef<string | null>(null);
const sceneSettingsCommitterActiveRef = useRef(true);
const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);
const polygonDatasetRouteOpenedRef = useRef(false);
@@ -536,6 +537,7 @@ export default function App() {
// produced a validated launch descriptor. Keep the old scene mounted
// before this point; now perform one controlled receiver teardown before
// accepting the already-ready archive.
selectedRecordedSessionIdRef.current = null;
setReplayTransitioning(true);
setRecordedReplay(null);
setRecordedReplayLabel(null);
@@ -553,6 +555,7 @@ export default function App() {
if (sourceSwitchBlockedRef.current) {
throw new Error(SPATIAL_SOURCE_SWITCH_BLOCKED_REASON);
}
selectedRecordedSessionIdRef.current = session.id;
setRecordedReplay(launch);
setRecordedReplayLabel(session.label);
setSourceUrl(launch.sourceUrl);
@@ -564,11 +567,28 @@ export default function App() {
if (outcome !== "accepted") setReplayTransitioning(false);
}, []);
const releaseRecordedReplayForDelete = useCallback(async (sessionId: string) => {
if (selectedRecordedSessionIdRef.current !== sessionId) return;
// Deleting the selected archive is a source transition. Unmount Rerun and
// recorded media first so no admission request can outlive the evidence.
selectedRecordedSessionIdRef.current = null;
setReplayTransitioning(true);
setRecordedReplay(null);
setRecordedReplayLabel(null);
setSourceUrl("");
setSourceDraft("");
await new Promise<void>((resolve) => {
window.requestAnimationFrame(() => window.setTimeout(resolve, 0));
});
if (selectedRecordedSessionIdRef.current === null) setReplayTransitioning(false);
}, []);
const activateAutomaticSpatialSource = useCallback(() => {
// A plugin-owned live/file-replay start must release any host-selected
// archive or manual URL before the new acquisition becomes non-terminal.
// This transition is an internal start boundary, not an operator source
// switch, so it intentionally does not consult the acquisition guard.
selectedRecordedSessionIdRef.current = null;
setReplayTransitioning(false);
setRecordedReplay(null);
setRecordedReplayLabel(null);
@@ -786,6 +806,7 @@ export default function App() {
onReplayAccepted={acceptRecordedReplay}
onReplaySettled={(_session, outcome) =>
settleRecordedReplaySwitch(outcome)}
onDeleteBegin={releaseRecordedReplayForDelete}
/>
) : activeDefinition.kind === "datasets" ? (
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
@@ -838,6 +859,7 @@ export default function App() {
onReplayAccepted: acceptRecordedReplay,
onReplaySettled: (_session, outcome) =>
settleRecordedReplaySwitch(outcome),
onDeleteBegin: releaseRecordedReplayForDelete,
}}
navigation={{
openView,
@@ -886,6 +908,7 @@ export default function App() {
title={sourceSwitchBlockedReason ?? undefined}
onClick={() => {
if (sourceSwitchBlockedRef.current) return;
selectedRecordedSessionIdRef.current = null;
setSourceDraft("");
setSourceUrl("");
setRecordedReplay(null);
@@ -901,6 +924,7 @@ export default function App() {
title={sourceSwitchBlockedReason ?? undefined}
onClick={() => {
if (sourceSwitchBlockedRef.current) return;
selectedRecordedSessionIdRef.current = null;
setSourceUrl(sourceDraft.trim());
setRecordedReplay(null);
setRecordedReplayLabel(null);