feat(producer): public exports for distributed render primitives#816
Conversation
ba95eb8 to
e58413e
Compare
07b1a71 to
60e67b7
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Review: public exports for distributed render primitives
Clean two-tier export strategy — focused @hyperframes/producer/distributed subpath for Lambda workers, plus re-exports from main for convenience. Separate esbuild entry keeps the distributed subpath from pulling in the in-process renderer's dependency tree. Package.json subpath export includes both import and types conditions. Test coverage verifies both import paths and confirms existing in-process exports are preserved. No concerns. LGTM.
vanceingalls
left a comment
There was a problem hiding this comment.
Public exports for the distributed render primitives. New @hyperframes/producer/distributed subpath bundles just the activity surface for thin chunk-runner images; main entry re-exports the three activity functions for callers that pin the full package. README example is a clean on-ramp. A few concerns on what's being made public and what should stay internal.
Strengths
build.mjs:57-68— separate esbuild entry point for the distributed subpath with the same workspace-alias plugin + externals as the main entry. Lambda chunk-runner images can import just this surface and skip the in-process renderer's transitive dep tree (puppeteer, postcss). Real packaging win.distributed.ts:163-170— re-exportingChunkSliceJson,CompositionMetadataJson,LockedRenderConfigas type-only exports gives adopters who deserialize a planDir'smeta/*.jsonthe actual shapes the producer wrote. No more "is itchunkIndexorindex?" guesswork.package.json:23-26— subpath export points at both the.jsentry AND the.d.tsdeclarations, so TypeScript consumers get types without an additional resolve step.publicExports.test.ts:269-277— pinning that the main entry still exportsexecuteRenderJob,createRenderJob,createCaptureSession,createFileServer. Catches a future refactor that accidentally drops an existing in-process surface (§17.1 contract from the design doc).README.md:106-138— the worked example shows the full controller→worker→controller flow with realistic-looking paths. Adopters can copy-paste this and have a starting skeleton; documentation that pays for itself.
Findings
important — applyRuntimeEnvSnapshot is exported, which leaks an implementation detail callers shouldn't use (distributed.ts:148-149)
applyRuntimeEnvSnapshot mutates process.env globally. Exporting it as a public API means adapter authors will reach for it ("I'll call it directly before my chunk runs") and skip the carefully-sequenced setup in renderChunk (file server creation BEFORE the env apply, RENDER_MODE_SCRIPT bake order). The cross-PR Rule 2 lens: every site that calls this function must do so in the right order — and exposing it as public API multiplies those sites without enforcement.
Either:
- Keep it module-internal.
renderChunkis the one and only legitimate caller; adapters who think they need to pre-apply env can pre-setprocess.envthemselves with plain Node primitives. - Document it as "internal — do not call directly; use
renderChunkfor the only correct apply sequence" but keep it exported for testability.
I'd take (1) — it's a private contract masquerading as public.
important — readWebGlVendorInfoFromCanvas exposes a Puppeteer Page in the public surface (distributed.ts:148-150, renderChunk.ts:538-572)
The function signature is (page: Page) => Promise<{vendor, renderer}>. Exporting it means every adopter who imports @hyperframes/producer/distributed gets a transitive type dependency on puppeteer-core — exactly the dep the focused subpath was designed to avoid (build.mjs:62 lists puppeteer as external). The type-only import doesn't drag puppeteer at runtime, but it does at typecheck.
If the intent is "expose for SwiftShader debugging," document it as such and put it on the subpath that already pulls puppeteer (the chunk-runner image needs it anyway). If the intent is "make it overridable in tests," that's an internal hook — don't export.
important — measurePlanDirBytes (#814) is exported, broadening the API contract (distributed.ts:129-130)
measurePlanDirBytes was added in #814 as a helper used by plan(). Exporting it as public API means any future change to its behavior (symlink handling, throw-vs-swallow on EACCES — see the #814 nit on swallowed errors) becomes a breaking change for adapters that grew to depend on it. There's no documented use case for adapters to call it directly. Keep internal until there's a concrete adopter ask.
nit — DEFAULT_CHUNK_SIZE / DEFAULT_MAX_PARALLEL_CHUNKS / PLAN_DIR_SIZE_LIMIT_BYTES exported as values, not type-only
These are configuration defaults. Exporting them as values means adopter code if (config.chunkSize ?? DEFAULT_CHUNK_SIZE) > 240 { ... } becomes legitimate, and a future bump of the default (e.g. 240→480 because Lambda's 15-min cap relaxed) is a behavior change for callers who copied the constant into their own logic. Either document the values as semver-stable, or expose a getDefaults() accessor that adopters call instead of importing the const.
nit — index.ts re-export naming collision risk (index.ts:78-92)
The main entry now exports plan, renderChunk, assemble at top level alongside in-process exports like executeRenderJob. The verb plan is generic enough to collide with adopter-side function names (plan() in a workflow definition, plan() in a state machine). Lower-risk option: namespace the distributed exports under a single object (distributed.plan, distributed.renderChunk) — matches the import naming and avoids polluting the global symbol space for callers who only want the in-process renderer.
Not a blocker — TypeScript IDEs catch the collisions and rename. But the bare-verb names will be noticed by adopters who use both.
nit — README example uses "/tmp/plan" hardcoded paths (README.md:117-128)
The example shows /tmp/plan and /tmp/chunks/0.mp4 — fine for documentation, but explicit os.tmpdir() + mkdtempSync would model the real-world pattern adopters need (per-invocation unique paths to avoid concurrent-render collisions). Doc-level polish.
Verdict: APPROVE
Reasoning: Packaging is right (separate subpath bundle for thin chunk-runners, type-only re-exports, README example), the test surface pins both the new exports AND the preservation of existing in-process exports. The over-exposed helpers (applyRuntimeEnvSnapshot, readWebGlVendorInfoFromCanvas, measurePlanDirBytes) are real API debt worth trimming before the package goes wide — but post-merge cleanup is reasonable since this is unreleased and the surface can still tighten.
— Vai
60e67b7 to
72407de
Compare
e58413e to
0f646aa
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Re-review
The export surface still works for the three activity functions + result types + the two "production-shaped" error codes. The published API for adapters is fine. One important gap on the de-overloaded error codes from #809.
Net-new finding
important — MISSING_PLAN_ARTIFACT, CHUNK_INDEX_OUT_OF_RANGE, MISSING_RUNTIME_ENV_SNAPSHOT, BROWSER_GPU_NOT_SOFTWARE not exported
#809 split the previously-overloaded PLAN_HASH_MISMATCH into five distinct codes so adapters can key retry policies independently — but distributed.ts:147-158 only re-exports two of them:
FFMPEG_VERSION_MISMATCH,
PLAN_HASH_MISMATCH,The three new structural codes (MISSING_PLAN_ARTIFACT, CHUNK_INDEX_OUT_OF_RANGE, MISSING_RUNTIME_ENV_SNAPSHOT) plus BROWSER_GPU_NOT_SOFTWARE (re-exported by renderChunk.ts from @hyperframes/engine, then needed at the public surface) are not in either the subpath or the main entry. The publicExports.test.ts only asserts the two that ARE exported (publicExports.test.ts:248-249).
Failure mode: an adapter that wants to if (err.code === MISSING_PLAN_ARTIFACT) (retry — could be a partial S3 download) vs. if (err.code === PLAN_HASH_MISMATCH) (don't retry — cross-version drift) has to hardcode the string literal because the constant isn't reachable. Hardcoded literals drift silently when the constant is renamed; this is exactly the failure mode the de-overloading was designed to prevent.
Fix: add the four codes to distributed.ts:147-158 alongside PLAN_HASH_MISMATCH, and extend publicExports.test.ts:243-254 to pin them.
Severity: important, not a blocker — adapters can still route on err.code string comparison; this is an ergonomic + drift-safety issue, not a correctness gap.
Prior findings status
None — this PR had no prior findings.
Verdict: APPROVE
Reasoning: Public surface functions are correctly wired and the test pins them. The missing error-code exports are a real but non-gating ergonomic gap — the codes are reachable via err.code strings. I'd take a follow-up PR rather than block on it, but flagging for the next PR in the series.
— Vai
72407de to
81975cc
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-stamping after rebase. LGTM — no changes from prior review.
81975cc to
5c789ef
Compare
0f646aa to
1f50bdb
Compare
552027b to
e506848
Compare
Merge activity
|
1f50bdb to
91a91f6
Compare
The base branch was changed.
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-stamping after restack. LGTM.

What
Phase 3 of the distributed rendering plan: §11 PR 3.6 public exports. This PR adds the published surface for
plan/renderChunk/assembleso adopters (AWS Lambda, Temporal, Cloud Run Jobs, K8s Jobs, plain SSH) canimportthem from a stable npm path.Why
PRs 3.1 through 3.5 added the three primitives and their plan-time validators inside
packages/producer/src/services/distributed/. Until this PR, those symbols weren't reachable from outside the producer — adopters that pin@hyperframes/producercouldn't access them. This PR makes the API public via two routes:@hyperframes/producer/distributed— focused subpath. Just the distributed primitives + their types + the typed non-retryable error classes. Sized for Lambda chunk-runner images that don't need the in-process renderer's transitive deps.@hyperframes/producer— main entry, re-exports the three activity functions + their result types. Callers that already pin the main package don't need a separate subpath import.How
packages/producer/src/distributed.tsre-export module — single source of truth for the distributed surface. Exportsplan,renderChunk,assemble, every supporting type (DistributedRenderConfig,PlanResult,ChunkResult,AssembleResult,LockedRenderConfig,CompositionMetadataJson,ChunkSliceJson), every constant (DEFAULT_CHUNK_SIZE,DEFAULT_MAX_PARALLEL_CHUNKS,PLAN_DIR_SIZE_LIMIT_BYTES), every error code (PLAN_TOO_LARGE,FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED,FFMPEG_VERSION_MISMATCH,PLAN_HASH_MISMATCH), the matching error classes, plus the validator helpers (rejectUnsupportedDistributedFormat,applyRuntimeEnvSnapshot,readWebGlVendorInfoFromCanvas,resolveChunkPlan,buildChunkSlices,measurePlanDirBytes).packages/producer/package.jsonadds the./distributedsubpath export pointing at./dist/distributed.js+./dist/distributed.d.ts. The package'sfiles: ["dist/"]already covers the new artifacts.packages/producer/build.mjsadds a third esbuild entry point forsrc/distributed.ts, with the same workspace-alias plugin + externals as the main entry. Thetsc --emitDeclarationOnlypass already emits the corresponding.d.ts.packages/producer/src/index.tsre-exports the three activity functions + their result types so the main entry stays a one-import on-ramp.packages/producer/README.mdgets a new "Distributed rendering" section with a worked example.What did NOT change
executeRenderJob, thehyperframes renderCLI, the producer HTTP routes — every existing export is untouched. The new exports are purely additive (§17.3).Test plan
packages/producer/src/services/distributed/publicExports.test.ts. 6 cases:bun test packages/producer/src/services/distributed/— 36 pass (all Phase 3 PRs combined).bun test packages/producer/src/— 338 pass, 1 fail (pre-existingwriteCompiledArtifactsflake onorigin/main).bun run --filter @hyperframes/producer typecheck— clean.bun run --filter @hyperframes/producer build— emitsdist/distributed.js,dist/distributed.d.ts, and source maps alongside the existing entry-point bundles.bunx oxlint+bunx oxfmt --check— clean on changed files.executeRenderJobis unchanged; PSNR baselines should hold.This is PR 6 of 6 of the Phase 3 stack:
services/distributed/plan.ts(feat(producer): add services/distributed/plan.ts #808)services/distributed/renderChunk.ts(feat(producer): add services/distributed/renderChunk.ts #809)services/distributed/assemble.ts(feat(producer): add services/distributed/assemble.ts #813)planDirsize cap (PLAN_TOO_LARGE) (feat(producer): enforce planDir size cap (PLAN_TOO_LARGE) #814)@hyperframes/producer/distributedsubpathAfter this lands,
import { plan, renderChunk, assemble } from "@hyperframes/producer/distributed"works end-to-end and Phase 4 (test fixtures +--mode=distributed-simulatedharness) can begin.🤖 Generated with Claude Code