Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/sdk/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions packages/sdk/src/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ─────────────────────────────────────────
Expand Down
20 changes: 20 additions & 0 deletions packages/studio/src/hooks/useSdkSession.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
50 changes: 45 additions & 5 deletions packages/studio/src/hooks/useSdkSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
Expand All @@ -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;
}
Loading