mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
* feat(lambda): add TypeScript SDK and CDK construct
Adds the client-side surface on top of the Phase 6a Lambda handler so
adopters can drive a deployed stack from Node without writing AWS-SDK
boilerplate:
- renderToLambda(opts) starts a Step Functions execution and returns a
handle. Does NOT poll.
- getRenderProgress({ executionArn }) returns a snapshot of progress,
frames rendered, cost (Lambda GB-seconds + SFN transitions), errors,
and the final output object once Assemble completes.
- deploySite({ projectDir, bucketName }) content-addresses the project
tree, tar.gzs it, and uploads to S3 with a HeadObject short-circuit so
re-renders of the same tree skip the tar+PUT.
- validateDistributedRenderConfig throws a typed InvalidConfigError
before StartExecution, so shape errors surface synchronously.
- computeRenderCost is exposed for callers who want to format cost out
of band.
Also ships HyperframesRenderStack, an aws-cdk-lib L2 construct that
emits the same topology as examples/aws-lambda/template.yaml. Lives on
the ./cdk subpath export so SDK-only consumers don't pull aws-cdk-lib
into their runtime graph (declared as an optional peer dependency).
Tests: 24 new unit tests across the SDK plus 9 CDK synth / contract /
snapshot tests. All 83 tests in packages/aws-lambda/src pass.
* refactor(lambda): /simplify pass on the SDK + CDK PR
Pulls shared logic out so the SDK doesn't re-invent things the handler
and the producer already have:
- `formatExtension` extracted to packages/aws-lambda/src/formatExtension.ts.
handler.ts and renderToLambda.ts both used identical 12-line copies of
this switch.
- `PLAN_PROJECT_DIR_SKIP_SEGMENTS` is now exported from
@hyperframes/producer/distributed. deploySite consumes it instead of
its own duplicate SKIP_TOP_LEVEL set; the two lists were trivially
identical and would have drifted silently.
- `FakeS3` + `drainBody` factored out of the two SDK test files into
src/sdk/__fixtures__/fakeS3.ts. Drops ~110 lines of test-file
duplication and gives future SDK tests a one-line FakeS3 import.
- S3 URI building in deploySite and renderToLambda routes through the
existing `formatS3Uri` helper instead of inline `s3://...`
concatenation; matches the convention already in handler.ts.
Net -133 lines across the touched files. All 83 aws-lambda tests still
pass; all 60 producer distributed tests still pass.
* fix(lambda): bump CDK test timeouts for CI cold-start synth
The bun:test default 5s timeout tripped the first CDK snapshot test
in CI when the cold-start `Template.fromStack(stack)` synth took ~5-8s
on the slowest GitHub Actions runner. Locally on a warm shell the
synth measures <1s, so the failure didn't reproduce until PR #909 hit
CI.
Two changes:
- Both CDK test files cache one synth in `beforeAll(..., 30000)` and
reuse the result across every test that uses the default props.
Each individual test now runs in microseconds (pure assertions
against the already-synthed template), so the 5s timeout no longer
applies on the hot path.
- The two contract tests that exercise non-default props
(reservedConcurrency, projectName) still synth fresh per-test; they
get a per-test `it(..., 30000)` timeout.
No behavior changes.
* fix(lambda): address PR review on SDK + CDK construct
Three correctness + ergonomics fixes raised in Vai's review:
- getRenderProgress over-counted SFN transitions by 3-5×. Step
Functions Standard Workflows bill per state-entry, not per
history event. Each Task produces ~5-7 history events
(Scheduled / Started / Succeeded / TaskStateExited / …);
counting `events.length` reported the runaway. Switch to
counting `*StateEntered` events explicitly.
- assembleComplete + outputFile detection was coupled to the
Lambda payload's `Action` field. Move both signals onto the
enclosing state name (`StateExited.name === "Assemble"`), which
is the state-machine identity rather than the Lambda event
contract. framesRendered increment moves to the same boundary
(RenderChunk state).
- SiteHandle now carries `bucketName` directly so README + CLI
callers don't have to re-parse `projectS3Uri.split("/")[2]`.
Test updates: getRenderProgress tests wrap renderChunk/assemble
events in matching StateEntered + StateExited pairs so the new
state-name-driven dispatch is exercised end-to-end. SiteHandle
fixture in renderToLambda.test.ts gets the new bucketName field.
All 83 aws-lambda tests still pass.
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
||
import { type BilledLambdaInvocation, computeRenderCost } from "./costAccounting.js";
|
||
|
||
describe("computeRenderCost", () => {
|
||
it("returns zero when nothing is billed", () => {
|
||
const result = computeRenderCost([], 0);
|
||
expect(result.accruedSoFarUsd).toBe(0);
|
||
expect(result.displayCost).toBe("$0.0000");
|
||
expect(result.breakdown.estimated).toBe(false);
|
||
});
|
||
|
||
it("computes Lambda GB-seconds × USD per GB-s", () => {
|
||
// 10 GiB × 6 s = 60 GB-s × $0.0000166667 = $0.001.
|
||
const invs: BilledLambdaInvocation[] = [
|
||
{ billedDurationMs: 6_000, memorySizeMb: 10_240, estimated: false },
|
||
];
|
||
const result = computeRenderCost(invs, 0);
|
||
expect(result.breakdown.lambdaUsd).toBeCloseTo(0.001, 4);
|
||
expect(result.breakdown.stepFunctionsUsd).toBe(0);
|
||
});
|
||
|
||
it("sums multiple Lambda invocations", () => {
|
||
const invs: BilledLambdaInvocation[] = [
|
||
{ billedDurationMs: 1_000, memorySizeMb: 1_024, estimated: false }, // 1 GB-s
|
||
{ billedDurationMs: 2_000, memorySizeMb: 2_048, estimated: false }, // 4 GB-s
|
||
];
|
||
const result = computeRenderCost(invs, 0);
|
||
// (1 + 4) × $0.0000166667 ≈ $0.0000833335 → rounds to $0.0001 at 4 dp.
|
||
expect(result.breakdown.lambdaUsd).toBe(0.0001);
|
||
});
|
||
|
||
it("adds Step Functions transition costs", () => {
|
||
const result = computeRenderCost([], 200);
|
||
expect(result.breakdown.stepFunctionsUsd).toBeCloseTo(200 * 0.000025, 6);
|
||
expect(result.breakdown.stepFunctionsUsd).toBeCloseTo(0.005, 4);
|
||
});
|
||
|
||
it("flags estimated=true when any invocation was estimated", () => {
|
||
const result = computeRenderCost(
|
||
[
|
||
{ billedDurationMs: 1_000, memorySizeMb: 1_024, estimated: true },
|
||
{ billedDurationMs: 1_000, memorySizeMb: 1_024, estimated: false },
|
||
],
|
||
10,
|
||
);
|
||
expect(result.breakdown.estimated).toBe(true);
|
||
});
|
||
|
||
it("formats USD to four decimal places", () => {
|
||
const result = computeRenderCost([], 1);
|
||
expect(result.displayCost).toBe("$0.0000");
|
||
const result2 = computeRenderCost([], 4_000);
|
||
expect(result2.displayCost).toBe("$0.1000");
|
||
});
|
||
|
||
it("does not include S3 in the breakdown", () => {
|
||
const result = computeRenderCost([], 0);
|
||
expect(result.breakdown.s3Estimate).toBe("not-included");
|
||
});
|
||
});
|