Files
hyperframes/packages/studio/src/utils/timelineCompositionInsert.ts
T
Miguel Ángel 2b65b4efce fix(studio): harden composition timeline reliability (#2615)
* fix(studio): preserve composition playback continuity

* feat(studio): drag compositions into the timeline

* fix(studio): collapse expanded composition move aliases

* fix(studio): make timeline cuts atomic

* fix(studio): group inspector gesture history

* test(studio): cover masked text selection

* fix(studio): harden composition timeline reliability

* fix(studio): satisfy CI source gates

* fix(studio): harden composition mutation requests
2026-07-17 14:15:30 -04:00

91 lines
3.0 KiB
TypeScript

import { createStudioSaveHttpError } from "./studioSaveDiagnostics";
import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
import type { RecordEditInput } from "./studioFileHistory";
import { buildProjectApiPath } from "./projectRouting";
interface TimelineCompositionInsertionResult {
path: string;
hostId: string;
before: string;
after: string;
version: string;
}
async function insertTimelineComposition(input: {
projectId: string;
targetPath: string;
sourcePath: string;
start: number;
track: number;
}): Promise<TimelineCompositionInsertionResult> {
const current = await fetch(
buildProjectApiPath(input.projectId, `/files/${encodeURIComponent(input.targetPath)}`),
);
if (!current.ok) {
throw await createStudioSaveHttpError(current, `Failed to read ${input.targetPath}`);
}
const snapshot = (await current.json()) as { version?: string };
if (typeof snapshot.version !== "string") throw new Error("Missing composition file version");
const response = await fetch(
buildProjectApiPath(
input.projectId,
`/file-mutations/insert-composition/${encodeURIComponent(input.targetPath)}`,
),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourcePath: input.sourcePath,
start: input.start,
track: input.track,
expectedVersion: snapshot.version,
}),
},
);
if (!response.ok) {
throw await createStudioSaveHttpError(response, "Failed to add composition to timeline");
}
return (await response.json()) as TimelineCompositionInsertionResult;
}
export async function commitTimelineCompositionInsertion(input: {
projectId: string;
targetPath: string;
sourcePath: string;
start: number;
track: number;
writeFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
recordEdit: (entry: RecordEditInput) => Promise<void>;
observeVersion?: (path: string, version: string | null) => void;
selectHost: (key: string) => void;
resync?: () => void;
refresh: () => void;
}): Promise<void> {
await serializeStudioFileMutation(input.writeFile, input.targetPath, async () => {
const result = await insertTimelineComposition(input);
input.observeVersion?.(input.targetPath, result.version);
try {
await input.recordEdit({
label: "Add composition to timeline",
kind: "timeline",
files: { [input.targetPath]: { before: result.before, after: result.after } },
});
} catch (error) {
await input.writeFile(input.targetPath, result.before, result.after);
throw error;
}
input.selectHost(`${input.targetPath}#${result.hostId}`);
try {
input.resync?.();
} catch (error) {
console.error("[Studio] Composition insertion committed but preview resync failed", error);
}
try {
input.refresh();
} catch (error) {
console.error("[Studio] Composition insertion committed but refresh failed", error);
}
});
}