mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(core): correct WAAPI rediscovery seek baselines and preview reuse invalidation
This commit is contained in:
@@ -398,7 +398,8 @@ async function runEmbeddedMode(
|
||||
userDataDir?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const { createStudioServer, resolveStudioBundle } = await import("../server/studioServer.js");
|
||||
const { createStudioServer, loadPreviewServerBuildSignature, resolveStudioBundle } =
|
||||
await import("../server/studioServer.js");
|
||||
|
||||
const pName = options?.projectName ?? basename(dir);
|
||||
const studioBundle = resolveStudioBundle();
|
||||
@@ -422,10 +423,17 @@ async function runEmbeddedMode(
|
||||
}
|
||||
|
||||
const { app } = createStudioServer({ projectDir: dir, projectName: pName });
|
||||
const serverBuildSignature = await loadPreviewServerBuildSignature();
|
||||
|
||||
let result: FindPortResult;
|
||||
try {
|
||||
result = await findPortAndServe(app.fetch, startPort, dir, !!options?.forceNew);
|
||||
result = await findPortAndServe(
|
||||
app.fetch,
|
||||
startPort,
|
||||
dir,
|
||||
!!options?.forceNew,
|
||||
serverBuildSignature,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
s.stop(c.error("Failed to start studio"));
|
||||
console.error();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createServer, type Server } from "node:net";
|
||||
import { PORT_PROBE_HOSTS, testPortOnAllHosts } from "./portUtils.js";
|
||||
import { createServer as createHttpServer, type Server as HttpServer } from "node:http";
|
||||
import { PORT_PROBE_HOSTS, detectHyperframesServer, testPortOnAllHosts } from "./portUtils.js";
|
||||
|
||||
const openServers: Server[] = [];
|
||||
const openHttpServers: HttpServer[] = [];
|
||||
|
||||
async function allocFreePort(): Promise<number> {
|
||||
const srv = createServer();
|
||||
@@ -24,9 +26,30 @@ afterEach(async () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
await Promise.all(
|
||||
openHttpServers.splice(0).map(
|
||||
(s) =>
|
||||
new Promise<void>((resolve) => {
|
||||
s.close(() => resolve());
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function startConfigProbeServer(payload: Record<string, unknown>): Promise<number> {
|
||||
const server = createHttpServer((_req, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(payload));
|
||||
});
|
||||
openHttpServers.push(server);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
return (server.address() as import("node:net").AddressInfo).port;
|
||||
}
|
||||
|
||||
describe("testPortOnAllHosts — real-socket behaviour (OS-dependent)", () => {
|
||||
// These exercise the real network stack. On Linux the buggy parallel
|
||||
// implementation reliably fails the first test (issue #309 repro); on
|
||||
@@ -97,3 +120,35 @@ describe("testPortOnAllHosts — sequential contract (platform-agnostic)", () =>
|
||||
expect(hostsProbed).toEqual(["127.0.0.1", "0.0.0.0"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectHyperframesServer", () => {
|
||||
it("treats same-project servers with a different server build signature as mismatch", async () => {
|
||||
const projectDir = "/tmp/demo-project";
|
||||
const port = await startConfigProbeServer({
|
||||
isHyperframes: true,
|
||||
projectName: "demo-project",
|
||||
projectDir,
|
||||
serverBuildSignature: "old-build",
|
||||
version: "0.6.42",
|
||||
});
|
||||
|
||||
const result = await detectHyperframesServer(port, projectDir, "new-build");
|
||||
|
||||
expect(result).toEqual({ type: "mismatch", projectName: "demo-project" });
|
||||
});
|
||||
|
||||
it("treats same-project servers with the same server build signature as match", async () => {
|
||||
const projectDir = "/tmp/demo-project";
|
||||
const port = await startConfigProbeServer({
|
||||
isHyperframes: true,
|
||||
projectName: "demo-project",
|
||||
projectDir,
|
||||
serverBuildSignature: "same-build",
|
||||
version: "0.6.42",
|
||||
});
|
||||
|
||||
const result = await detectHyperframesServer(port, projectDir, "same-build");
|
||||
|
||||
expect(result).toEqual({ type: "match" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,6 +99,7 @@ interface HyperframesConfigResponse {
|
||||
isHyperframes: boolean;
|
||||
projectName: string;
|
||||
projectDir: string;
|
||||
serverBuildSignature?: string | null;
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -114,6 +115,7 @@ export type DetectionResult =
|
||||
export function detectHyperframesServer(
|
||||
port: number,
|
||||
normalizedProjectDir: string,
|
||||
expectedServerBuildSignature: string | null = null,
|
||||
): Promise<DetectionResult> {
|
||||
return new Promise<DetectionResult>((resolveResult) => {
|
||||
const req = http.get(
|
||||
@@ -152,6 +154,12 @@ export function detectHyperframesServer(
|
||||
const normalize = (p: string) => resolve(p).replace(/\\/g, "/").toLowerCase();
|
||||
|
||||
if (normalize(json.projectDir) === normalizedProjectDir) {
|
||||
if (
|
||||
expectedServerBuildSignature !== null &&
|
||||
json.serverBuildSignature !== expectedServerBuildSignature
|
||||
) {
|
||||
return resolveResult({ type: "mismatch", projectName: json.projectName });
|
||||
}
|
||||
return resolveResult({ type: "match" });
|
||||
}
|
||||
|
||||
@@ -327,6 +335,7 @@ export async function findPortAndServe(
|
||||
startPort: number,
|
||||
projectDir: string,
|
||||
forceNew: boolean,
|
||||
expectedServerBuildSignature: string | null = null,
|
||||
): Promise<FindPortResult> {
|
||||
const { createAdaptorServer } = await import("@hono/node-server");
|
||||
const normalizedDir = resolve(projectDir).replace(/\\/g, "/").toLowerCase();
|
||||
@@ -366,7 +375,11 @@ export async function findPortAndServe(
|
||||
|
||||
// Port is occupied — probe for existing HyperFrames instance
|
||||
if (!forceNew) {
|
||||
const detection = await detectHyperframesServer(port, normalizedDir);
|
||||
const detection = await detectHyperframesServer(
|
||||
port,
|
||||
normalizedDir,
|
||||
expectedServerBuildSignature,
|
||||
);
|
||||
if (detection.type === "match") {
|
||||
return { type: "already-running", port };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
@@ -16,6 +17,21 @@ export async function loadRuntimeSource(): Promise<string | null> {
|
||||
return (await buildFromSource()) ?? (await getInlinedRuntime()) ?? readPrebuiltArtifact();
|
||||
}
|
||||
|
||||
export async function loadRuntimeSourceSignature(): Promise<string | null> {
|
||||
const source = await loadRuntimeSource();
|
||||
if (!source) return null;
|
||||
return createHash("sha256").update(source).digest("hex");
|
||||
}
|
||||
|
||||
export function hashSignatureParts(parts: Array<string | null | undefined>): string {
|
||||
const hash = createHash("sha256");
|
||||
for (const part of parts) {
|
||||
hash.update(part ?? "");
|
||||
hash.update("\n--hf-signature-part--\n");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
// ── Strategy 1: live build from source (dev only) ──────────────────────────
|
||||
|
||||
const ENTRY_TS = resolve(__dirname, "..", "..", "..", "core", "src", "runtime", "entry.ts");
|
||||
|
||||
@@ -10,7 +10,11 @@ import { streamSSE } from "hono/streaming";
|
||||
import { existsSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
||||
import { resolve, join, basename } from "node:path";
|
||||
import { createProjectWatcher, type ProjectWatcher } from "./fileWatcher.js";
|
||||
import { loadRuntimeSource } from "./runtimeSource.js";
|
||||
import {
|
||||
hashSignatureParts,
|
||||
loadRuntimeSource,
|
||||
loadRuntimeSourceSignature,
|
||||
} from "./runtimeSource.js";
|
||||
import { VERSION as version } from "../version.js";
|
||||
import { emitStudioRenderComplete, emitStudioRenderError } from "./studioRenderTelemetry.js";
|
||||
import {
|
||||
@@ -184,6 +188,25 @@ export interface StudioServer {
|
||||
watcher: ProjectWatcher;
|
||||
}
|
||||
|
||||
export async function loadPreviewServerBuildSignature(): Promise<string> {
|
||||
const runtimeSignature = await loadRuntimeSourceSignature();
|
||||
const studioBundle = resolveStudioBundle();
|
||||
const studioIndex =
|
||||
studioBundle.available && existsSync(studioBundle.indexPath)
|
||||
? readFileSync(studioBundle.indexPath, "utf-8")
|
||||
: "";
|
||||
return hashSignatureParts([
|
||||
version,
|
||||
runtimeSignature,
|
||||
studioIndex,
|
||||
createStudioServer.toString(),
|
||||
createStudioApi.toString(),
|
||||
createProjectSignature.toString(),
|
||||
getMimeType.toString(),
|
||||
getElementScreenshotClip.toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
const { projectDir, projectName } = options;
|
||||
const projectId = projectName || basename(projectDir);
|
||||
@@ -445,12 +468,17 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
// HyperFrames instances and reuse them instead of spawning duplicates.
|
||||
// See portUtils.ts detectHyperframesServer() for the consumer.
|
||||
app.get("/__hyperframes_config", (c) => {
|
||||
return c.json({
|
||||
isHyperframes: true,
|
||||
projectName: projectId,
|
||||
projectDir: projectDir,
|
||||
version,
|
||||
});
|
||||
const serve = async () => {
|
||||
const serverBuildSignature = await loadPreviewServerBuildSignature();
|
||||
return c.json({
|
||||
isHyperframes: true,
|
||||
projectName: projectId,
|
||||
projectDir: projectDir,
|
||||
serverBuildSignature,
|
||||
version,
|
||||
});
|
||||
};
|
||||
return serve();
|
||||
});
|
||||
|
||||
// CLI-specific routes (before shared API)
|
||||
|
||||
Reference in New Issue
Block a user