fix(replay): recover failed archives and release deleted sessions
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface ObservationSessionReplayCallbacks {
|
||||
session: ObservationSessionSummary,
|
||||
outcome: ObservationReplayOutcome,
|
||||
) => void | Promise<void>;
|
||||
onDeleteBegin?: (sessionId: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ObservationSessionSelect({
|
||||
@@ -114,6 +115,7 @@ export function ObservationSessionSelect({
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
onDeleteBegin,
|
||||
}: ObservationSessionReplayCallbacks & {
|
||||
limit?: number;
|
||||
disabled?: boolean;
|
||||
@@ -127,6 +129,7 @@ export function ObservationSessionSelect({
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
onDeleteBegin,
|
||||
});
|
||||
const triggerCopy = sessions.replayProgress
|
||||
? progressCopy(sessions.replayProgress.phase)
|
||||
@@ -313,6 +316,7 @@ export function ObservationSessionArchive({
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
onDeleteBegin,
|
||||
}: ObservationSessionReplayCallbacks & {
|
||||
limit?: number;
|
||||
labsOnly?: boolean;
|
||||
@@ -327,6 +331,7 @@ export function ObservationSessionArchive({
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
onDeleteBegin,
|
||||
});
|
||||
const items = sessions.items;
|
||||
|
||||
|
||||
@@ -52,6 +52,20 @@ export interface ObservationReplayCoordinator {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export async function deleteObservationSessionAfterTeardown(
|
||||
sessionId: string,
|
||||
{
|
||||
onDeleteBegin,
|
||||
deleteSession = deleteObservationSession,
|
||||
}: {
|
||||
onDeleteBegin?: (sessionId: string) => void | Promise<void>;
|
||||
deleteSession?: (sessionId: string) => Promise<void>;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
await onDeleteBegin?.(sessionId);
|
||||
await deleteSession(sessionId);
|
||||
}
|
||||
|
||||
export interface ObservationPreparationPollingOptions {
|
||||
signal: AbortSignal;
|
||||
fetcher?: ObservationSessionFetch;
|
||||
@@ -375,6 +389,7 @@ export function useObservationSessions({
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
onDeleteBegin,
|
||||
}: {
|
||||
limit?: number;
|
||||
scope?: ObservationSessionScope;
|
||||
@@ -392,6 +407,8 @@ export function useObservationSessions({
|
||||
session: ObservationSessionSummary,
|
||||
outcome: ObservationReplayOutcome,
|
||||
) => void | Promise<void>;
|
||||
/** Releases an active viewer before its evidence and derived cache are deleted. */
|
||||
onDeleteBegin?: (sessionId: string) => void | Promise<void>;
|
||||
} = {}): ObservationSessionsController {
|
||||
const [items, setItems] = useState<ObservationSessionSummary[]>([]);
|
||||
const [state, setState] = useState<ObservationSessionsLoadState>("idle");
|
||||
@@ -606,7 +623,7 @@ export function useObservationSessions({
|
||||
setDeletingSessionId(sessionId);
|
||||
setError(null);
|
||||
try {
|
||||
await deleteObservationSession(sessionId);
|
||||
await deleteObservationSessionAfterTeardown(sessionId, { onDeleteBegin });
|
||||
if (!mounted.current) return false;
|
||||
setItems((current) => current.filter((item) => item.id !== sessionId));
|
||||
setFailedSessionId((current) => current === sessionId ? null : current);
|
||||
@@ -617,7 +634,7 @@ export function useObservationSessions({
|
||||
} finally {
|
||||
if (mounted.current) setDeletingSessionId(null);
|
||||
}
|
||||
}, [deletingSessionId, replayingSessionId]);
|
||||
}, [deletingSessionId, onDeleteBegin, replayingSessionId]);
|
||||
|
||||
return {
|
||||
items,
|
||||
|
||||
@@ -583,6 +583,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
onReplayBegin: props.sessionArchive.onReplayBegin,
|
||||
onReplayAccepted: props.sessionArchive.onReplayAccepted,
|
||||
onReplaySettled: props.sessionArchive.onReplaySettled,
|
||||
onDeleteBegin: props.sessionArchive.onDeleteBegin,
|
||||
});
|
||||
const publishedWorks = useMemo(
|
||||
() => sessions.items.filter((session) => (
|
||||
|
||||
Reference in New Issue
Block a user