mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(sdk): ws-3 — reorderElements op (batch z-index update) (#1502)
* feat(sdk): ws-3 — reorderElements op (batch z-index update) Adds the reorderElements EditOp: each entry sets inline zIndex on one element. Last-write-wins per target so a duplicated target collapses to a single zIndex patch. Positioning is unchanged — z-index only takes effect on non-static elements, so the caller must ensure the target is positioned. Also fixes single-dispatch undo to reverse the inverse patch list (parity with batch()): an op emitting multiple patches whose undo order matters — a duplicated reorderElements target, an aliased multi-target, or a nested parent+child removeElement — must undo in reverse application order, or undo lands on an intermediate value / drops a subtree. validateOp resolves every entry target (E_TARGET_NOT_FOUND for unknown ids; empty entries is a clean no-op). Tests cover set/inverse/validate/duplicate-target. Rebuilt standalone on main (reorderElements only depends on handleSetStyle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sdk): remove unused HTTP persist adapter The HTTP PersistAdapter (createHttpAdapter) was dead weight after the studio cutover went single-writer (ws-4): Studio's writeProjectFile is the sole writer and useSdkSession opens with no persist queue, so the adapter's write/flush/ listVersions/loadFrom were never used — only read() was, to fetch the composition source. Replace those two read() calls with a direct optional fetch (GET /files/<path>?optional=1) and drop the adapter + its export-map entries. Saved for later re-introduction (when a non-Studio SDK host needs server-backed persist) at docs/hyperframes/plans/sdk-http-adapter/ (outside the repo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(studio): resolver shadow for z-index reorder targets The z-index reorder commit takes the server path (no SDK persist), but the resolver-shadow tripwire is decoupled from cutover — so it should still record whether the SDK resolves each reordered element (reorderElements' targets), the same as timing/delete already do before their cutover gate. This gives wild resolver-parity telemetry on z-index targets before z-index reorder is cut over. Threads an onReorderShadow callback (sdkSession-bound, mirrors onTrySdkDelete) from useDomEditSession → useDomEditCommits → useElementLifecycleOps, called with the reordered elements' hf-ids in handleDomZIndexReorderCommit. Read-only, divergence-only, never throws — same contract as recordResolverParity elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): guard project-file read path against traversal (CodeQL CSRF) readProjectFileOptional interpolated a user-influenced composition path into the fetch URL, which CodeQL flagged as client-side request forgery. Reject NUL/`..` up front (mirrors the existing guard in timelineEditingHelpers) and encodeURIComponent the projectId too, so both values stay confined to single segments of the same-origin URL. Unsafe path → undefined (graceful for the optional read). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
de87f3932e
commit
28bfe09f21
@@ -1,11 +1,32 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { openComposition } from "@hyperframes/sdk";
|
||||
import { createHttpAdapter } from "@hyperframes/sdk/adapters/http";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import { readStudioFileChangePath } from "../components/editor/manualEdits";
|
||||
import { isSelfWriteEcho } from "./sdkSelfWriteRegistry";
|
||||
|
||||
/**
|
||||
* Read a project file's content, or undefined on a non-2xx (optional read).
|
||||
* Replaces the removed SDK http adapter's `read()` — the only thing Studio used
|
||||
* it for (Studio is the sole writer, so the adapter's write path was dead).
|
||||
*/
|
||||
async function readProjectFileOptional(
|
||||
projectId: string,
|
||||
path: string,
|
||||
): Promise<string | undefined> {
|
||||
// Reject traversal / NUL before building the request URL — `path` is a
|
||||
// user-influenced composition path (mirrors the guard in timelineEditingHelpers,
|
||||
// and closes the CodeQL client-side-request-forgery flag). encodeURIComponent
|
||||
// already confines both values to single segments of this same-origin URL.
|
||||
if (path.includes("\0") || path.includes("..")) return undefined;
|
||||
const res = await fetch(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
|
||||
);
|
||||
if (!res.ok) return undefined;
|
||||
const data = (await res.json()) as { content?: string };
|
||||
return typeof data.content === "string" ? data.content : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an external file-change payload targets the active composition and
|
||||
* the SDK session must be re-opened to pick up the new content.
|
||||
@@ -18,8 +39,8 @@ export function shouldReloadSdkSession(payload: unknown, activeCompPath: string
|
||||
/**
|
||||
* Stage 7 Step 3a — SDK session wired to the active composition.
|
||||
*
|
||||
* Creates an SDK Composition backed by createHttpAdapter on every
|
||||
* (projectId, activeCompPath) change, disposes the old one on cleanup, and
|
||||
* Creates an SDK Composition (reading the file via the project files API) on
|
||||
* every (projectId, activeCompPath) change, disposes the old one on cleanup, and
|
||||
* re-opens it when the active composition file changes on disk (code editor,
|
||||
* agent, or server-side patch) so the in-memory linkedom document never goes
|
||||
* stale. The session has NO persist queue — Studio is the sole file writer; see
|
||||
@@ -81,10 +102,7 @@ export function useSdkSession(
|
||||
useEffect(() => {
|
||||
if (!activeCompPath) return;
|
||||
const compPath = activeCompPath;
|
||||
const readAdapter =
|
||||
projectId != null
|
||||
? createHttpAdapter({ projectFilesUrl: `/api/projects/${projectId}` })
|
||||
: null;
|
||||
const readProjectId = projectId ?? null;
|
||||
const handler = (payload?: unknown) => {
|
||||
if (!shouldReloadSdkSession(payload, compPath)) return;
|
||||
const withinWindow =
|
||||
@@ -96,13 +114,12 @@ export function useSdkSession(
|
||||
const payloadContent = readFileChangeContent(payload);
|
||||
// Prefer payload content; otherwise re-read so the decision is by IDENTITY
|
||||
// (an undo's reverted bytes won't match a registered self-write → reload).
|
||||
if (payloadContent != null || !readAdapter) {
|
||||
if (payloadContent != null || readProjectId == null) {
|
||||
decide(payloadContent);
|
||||
return;
|
||||
}
|
||||
readAdapter
|
||||
.read(compPath)
|
||||
.then((c) => decide(typeof c === "string" ? c : null))
|
||||
readProjectFileOptional(readProjectId, compPath)
|
||||
.then((c) => decide(c ?? null))
|
||||
.catch(() => decide(null));
|
||||
};
|
||||
if (import.meta.hot) {
|
||||
@@ -126,11 +143,7 @@ export function useSdkSession(
|
||||
let cancelled = false;
|
||||
const compRef = { current: null as Composition | null };
|
||||
|
||||
const adapter = createHttpAdapter({
|
||||
projectFilesUrl: `/api/projects/${projectId}`,
|
||||
});
|
||||
adapter
|
||||
.read(activeCompPath)
|
||||
readProjectFileOptional(projectId, activeCompPath)
|
||||
.then(async (content) => {
|
||||
if (cancelled || typeof content !== "string") return;
|
||||
// No persist queue: Studio's writeProjectFile (via sdkCutover's
|
||||
|
||||
Reference in New Issue
Block a user