mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(sdk,studio): stage 7 step 3a — persistPath + SDK session reload-on-change (#1449)
* feat(sdk,studio): stage 7 step 3a — persistPath + SDK session reload-on-change Stage 7 Step 3a — SDK plumbing for routing Studio commits through the SDK session. No behavior change: the session stays idle (no op routed yet). SDK: - Add OpenCompositionOptions.persistPath; thread to createPersistQueue so the persist queue writes back to the composition's real path instead of the "composition.html" default (blocker A). Studio (useSdkSession): - Pass persistPath = activeCompPath so a future dispatch persists the right file. - Re-open the session when the active composition file changes on disk (HMR hf:file-change / SSE file-change), scoped to activeCompPath, so the in-memory linkedom document never goes stale under code-editor/agent/server edits (blocker C). Re-opening is additive while the session is idle; 3c must add self-write suppression once dispatch writes. Tests: SDK persistPath default + override; shouldReloadSdkSession path-match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sdk): document persistPath as immutable for session lifetime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Miguel Ángel
parent
92e2c8ce6a
commit
5fe87cc39b
@@ -39,6 +39,8 @@ import type { PersistQueueModule } from "./persist-queue.js";
|
||||
|
||||
export interface OpenCompositionOptions {
|
||||
persist?: PersistAdapter;
|
||||
/** Adapter path the persist queue writes to. Default: "composition.html". Immutable for the session lifetime. */
|
||||
persistPath?: string;
|
||||
preview?: PreviewAdapter;
|
||||
/** T3 embedded mode: override-set applied on top of the base template. */
|
||||
overrides?: OverrideSet;
|
||||
@@ -502,6 +504,7 @@ export async function openComposition(
|
||||
|
||||
if (opts?.persist) {
|
||||
const pq = createPersistQueue(session, opts.persist, {
|
||||
path: opts.persistPath,
|
||||
onError: (e) => session._fireError(e),
|
||||
});
|
||||
session.attachPersistQueue(pq);
|
||||
|
||||
@@ -240,6 +240,33 @@ describe("persist adapter", () => {
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(errors).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("defaults the write path to composition.html when persistPath is omitted", async () => {
|
||||
const adapter = createMemoryAdapter();
|
||||
const writeSpy = vi.spyOn(adapter, "write");
|
||||
|
||||
const comp = await openComposition(BASE_HTML, { persist: adapter });
|
||||
comp.setStyle("hf-title", { color: "#f00" });
|
||||
await comp.flush();
|
||||
|
||||
const [path] = writeSpy.mock.calls[0] as [string, string];
|
||||
expect(path).toBe("composition.html");
|
||||
});
|
||||
|
||||
it("writes to persistPath when supplied", async () => {
|
||||
const adapter = createMemoryAdapter();
|
||||
const writeSpy = vi.spyOn(adapter, "write");
|
||||
|
||||
const comp = await openComposition(BASE_HTML, {
|
||||
persist: adapter,
|
||||
persistPath: "scenes/intro.html",
|
||||
});
|
||||
comp.setStyle("hf-title", { color: "#f00" });
|
||||
await comp.flush();
|
||||
|
||||
const [path] = writeSpy.mock.calls[0] as [string, string];
|
||||
expect(path).toBe("scenes/intro.html");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── T3 embedded mode (override-set) ─────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldReloadSdkSession } from "./useSdkSession";
|
||||
|
||||
describe("shouldReloadSdkSession", () => {
|
||||
it("reloads when the changed file is the active composition", () => {
|
||||
expect(shouldReloadSdkSession({ path: "scenes/intro.html" }, "scenes/intro.html")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores changes to other files", () => {
|
||||
expect(shouldReloadSdkSession({ path: "styles/main.css" }, "scenes/intro.html")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores changes when no composition is active", () => {
|
||||
expect(shouldReloadSdkSession({ path: "scenes/intro.html" }, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores payloads with no resolvable path", () => {
|
||||
expect(shouldReloadSdkSession({}, "scenes/intro.html")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,57 @@ import { useState, useEffect } 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";
|
||||
|
||||
/**
|
||||
* Stage 7 Step 1 — SDK session wired to the active composition.
|
||||
* 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.
|
||||
*/
|
||||
export function shouldReloadSdkSession(payload: unknown, activeCompPath: string | null): boolean {
|
||||
if (!activeCompPath) return false;
|
||||
return readStudioFileChangePath(payload) === activeCompPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The session is idle until Step 3 routes dispatch ops through it.
|
||||
* (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 persist queue writes back to `activeCompPath` (not the
|
||||
* "composition.html" default).
|
||||
*
|
||||
* The session is idle until Step 3c routes dispatch ops through it; re-opening
|
||||
* is therefore purely additive — no SDK self-write exists yet, so there is no
|
||||
* persist echo. Step 3c must add self-write suppression once dispatch writes.
|
||||
*/
|
||||
export function useSdkSession(
|
||||
projectId: string | null,
|
||||
activeCompPath: string | null,
|
||||
): Composition | null {
|
||||
const [session, setSession] = useState<Composition | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
|
||||
// ── Re-open on external change to the active composition ──
|
||||
useEffect(() => {
|
||||
if (!activeCompPath) return;
|
||||
const handler = (payload?: unknown) => {
|
||||
if (shouldReloadSdkSession(payload, activeCompPath)) {
|
||||
setReloadToken((t) => t + 1);
|
||||
}
|
||||
};
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on("hf:file-change", handler);
|
||||
return () => import.meta.hot?.off?.("hf:file-change", handler);
|
||||
}
|
||||
// SSE fallback for the embedded studio server.
|
||||
const es = new EventSource("/api/events");
|
||||
es.addEventListener("file-change", handler);
|
||||
return () => es.close();
|
||||
}, [activeCompPath]);
|
||||
|
||||
// ── Open / re-open the session ──
|
||||
useEffect(() => {
|
||||
if (!projectId || !activeCompPath) {
|
||||
setSession(null);
|
||||
@@ -32,7 +69,10 @@ export function useSdkSession(
|
||||
.read(activeCompPath)
|
||||
.then(async (content) => {
|
||||
if (cancelled || typeof content !== "string") return;
|
||||
comp = await openComposition(content, { persist: adapter });
|
||||
comp = await openComposition(content, {
|
||||
persist: adapter,
|
||||
persistPath: activeCompPath,
|
||||
});
|
||||
comp.on("persist:error", (e) => {
|
||||
console.warn("[sdk] persist:error", e.error);
|
||||
});
|
||||
@@ -52,7 +92,7 @@ export function useSdkSession(
|
||||
const c = comp;
|
||||
if (c) void c.flush().finally(() => c.dispose());
|
||||
};
|
||||
}, [projectId, activeCompPath]);
|
||||
}, [projectId, activeCompPath, reloadToken]);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user