mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-13 20:58:33 +00:00
* perf(cli): cloud render checks render status every 2 seconds instead of 10 A finished render was noticed up to 10 s late. Polling every 2 s cuts that wait to at most 2 s and stays well under the status endpoint's rate limit. `--poll-interval` still overrides it. * perf(cli): cloud render downloads the finished video over parallel connections The first request asks for the first 8 MB. When the server answers 206 with the full size, the rest of the file is fetched in up to 7 more ranged requests at once and each part is written at its offset into the staged file. A server that ignores Range (200), a small file, or an empty file is still fetched in one request. A failed part cancels the others and the existing output is kept, as before.
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
DEFAULT_MAX_WAIT_MS,
|
|
DEFAULT_POLL_INTERVAL_MS,
|
|
PollTimeoutError,
|
|
isTerminal,
|
|
pollUntilTerminal,
|
|
} from "./poll.js";
|
|
import type { HyperframesCloudClient } from "./_gen/client.js";
|
|
import type { HyperframesRenderDetail } from "./_gen/types.js";
|
|
|
|
function makeDetail(overrides: Partial<HyperframesRenderDetail>): HyperframesRenderDetail {
|
|
return {
|
|
render_id: "hfr_test",
|
|
status: "queued",
|
|
format: "mp4",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a stub client that returns the supplied details in order. */
|
|
function stubClient(details: HyperframesRenderDetail[]): HyperframesCloudClient {
|
|
const stack = [...details];
|
|
return {
|
|
async getRender() {
|
|
const next = stack.shift();
|
|
if (!next) throw new Error("ran out of stubbed responses");
|
|
return next;
|
|
},
|
|
} as unknown as HyperframesCloudClient;
|
|
}
|
|
|
|
describe("cloud/poll", () => {
|
|
describe("isTerminal", () => {
|
|
it("treats completed/failed as terminal", () => {
|
|
expect(isTerminal("completed")).toBe(true);
|
|
expect(isTerminal("failed")).toBe(true);
|
|
});
|
|
it("treats queued/rendering as non-terminal", () => {
|
|
expect(isTerminal("queued")).toBe(false);
|
|
expect(isTerminal("rendering")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("defaults", () => {
|
|
it("matches the documented 2s / 60min defaults", () => {
|
|
expect(DEFAULT_POLL_INTERVAL_MS).toBe(2_000);
|
|
expect(DEFAULT_MAX_WAIT_MS).toBe(60 * 60 * 1000);
|
|
});
|
|
});
|
|
|
|
describe("pollUntilTerminal", () => {
|
|
it("returns immediately when the first poll is terminal", async () => {
|
|
const client = stubClient([makeDetail({ status: "completed" })]);
|
|
const sleep = vi.fn(async () => {});
|
|
const result = await pollUntilTerminal(client, "hfr_test", { sleep });
|
|
expect(result.status).toBe("completed");
|
|
expect(sleep).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("sleeps between non-terminal polls and returns on the terminal one", async () => {
|
|
const client = stubClient([
|
|
makeDetail({ status: "queued" }),
|
|
makeDetail({ status: "rendering" }),
|
|
makeDetail({ status: "completed" }),
|
|
]);
|
|
const sleep = vi.fn(async () => {});
|
|
const now = (() => {
|
|
let t = 0;
|
|
return () => {
|
|
t += 1000;
|
|
return t;
|
|
};
|
|
})();
|
|
const ticks: string[] = [];
|
|
const result = await pollUntilTerminal(client, "hfr_test", {
|
|
sleep,
|
|
now,
|
|
intervalMs: 5_000,
|
|
onTick: (d) => ticks.push(d.status),
|
|
});
|
|
expect(result.status).toBe("completed");
|
|
expect(ticks).toEqual(["queued", "rendering", "completed"]);
|
|
// Two sleeps: one after queued, one after rendering. None after the
|
|
// terminal completed response.
|
|
expect(sleep).toHaveBeenCalledTimes(2);
|
|
expect(sleep).toHaveBeenCalledWith(5_000);
|
|
});
|
|
|
|
it("throws PollTimeoutError when elapsed exceeds maxWaitMs", async () => {
|
|
const client = stubClient([
|
|
makeDetail({ status: "queued" }),
|
|
makeDetail({ status: "rendering" }),
|
|
makeDetail({ status: "rendering" }),
|
|
]);
|
|
const sleep = vi.fn(async () => {});
|
|
// Each `now()` returns +500ms; total elapses past 1s on the second
|
|
// call, so maxWaitMs=1 triggers immediately.
|
|
const now = (() => {
|
|
let t = 0;
|
|
return () => {
|
|
t += 1000;
|
|
return t;
|
|
};
|
|
})();
|
|
await expect(
|
|
pollUntilTerminal(client, "hfr_test", {
|
|
sleep,
|
|
now,
|
|
intervalMs: 5_000,
|
|
maxWaitMs: 1,
|
|
}),
|
|
).rejects.toBeInstanceOf(PollTimeoutError);
|
|
});
|
|
|
|
it("aborts when the AbortSignal is fired", async () => {
|
|
const client = stubClient([makeDetail({ status: "queued" })]);
|
|
const controller = new AbortController();
|
|
controller.abort(new Error("user cancelled"));
|
|
await expect(
|
|
pollUntilTerminal(client, "hfr_test", { signal: controller.signal }),
|
|
).rejects.toThrow("user cancelled");
|
|
});
|
|
});
|
|
});
|