Skip to content

feat(producer): public exports for distributed render primitives#816

Merged
jrusso1020 merged 1 commit into
mainfrom
05-13-feat_producer_export_distributed_render_primitives
May 14, 2026
Merged

feat(producer): public exports for distributed render primitives#816
jrusso1020 merged 1 commit into
mainfrom
05-13-feat_producer_export_distributed_render_primitives

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented May 13, 2026

What

Phase 3 of the distributed rendering plan: §11 PR 3.6 public exports. This PR adds the published surface for plan / renderChunk / assemble so adopters (AWS Lambda, Temporal, Cloud Run Jobs, K8s Jobs, plain SSH) can import them 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/producer couldn't access them. This PR makes the API public via two routes:

  1. @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.
  2. @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

  • New packages/producer/src/distributed.ts re-export module — single source of truth for the distributed surface. Exports plan, 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.json adds the ./distributed subpath export pointing at ./dist/distributed.js + ./dist/distributed.d.ts. The package's files: ["dist/"] already covers the new artifacts.
  • packages/producer/build.mjs adds a third esbuild entry point for src/distributed.ts, with the same workspace-alias plugin + externals as the main entry. The tsc --emitDeclarationOnly pass already emits the corresponding .d.ts.
  • packages/producer/src/index.ts re-exports the three activity functions + their result types so the main entry stays a one-import on-ramp.
  • packages/producer/README.md gets a new "Distributed rendering" section with a worked example.

What did NOT change

executeRenderJob, the hyperframes render CLI, the producer HTTP routes — every existing export is untouched. The new exports are purely additive (§17.3).

Test plan

  • Unit tests added — packages/producer/src/services/distributed/publicExports.test.ts. 6 cases:
    • Subpath exports the three activity functions.
    • Subpath exports the chunking helpers + constants.
    • Subpath exports the non-retryable error codes + classes.
    • Subpath exports the input-validation helpers.
    • Main entry re-exports the three activity functions.
    • Main entry preserves the existing in-process exports (§17.1).
  • bun test packages/producer/src/services/distributed/ — 36 pass (all Phase 3 PRs combined).
  • bun test packages/producer/src/ — 338 pass, 1 fail (pre-existing writeCompiledArtifacts flake on origin/main).
  • bun run --filter @hyperframes/producer typecheck — clean.
  • bun run --filter @hyperframes/producer build — emits dist/distributed.js, dist/distributed.d.ts, and source maps alongside the existing entry-point bundles.
  • bunx oxlint + bunx oxfmt --check — clean on changed files.
  • Producer Docker regression harness — pending CI. executeRenderJob is unchanged; PSNR baselines should hold.

This is PR 6 of 6 of the Phase 3 stack:

After this lands, import { plan, renderChunk, assemble } from "@hyperframes/producer/distributed" works end-to-end and Phase 4 (test fixtures + --mode=distributed-simulated harness) can begin.

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 changed the title feat(producer): export distributed render primitives feat(producer): public exports for distributed render primitives May 13, 2026
@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_export_distributed_render_primitives branch from ba95eb8 to e58413e Compare May 13, 2026 23:02
@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_refuse_distributed-unsupported_formats_early branch from 07b1a71 to 60e67b7 Compare May 13, 2026 23:02
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-exporting ChunkSliceJson, CompositionMetadataJson, LockedRenderConfig as type-only exports gives adopters who deserialize a planDir's meta/*.json the actual shapes the producer wrote. No more "is it chunkIndex or index?" guesswork.
  • package.json:23-26 — subpath export points at both the .js entry AND the .d.ts declarations, so TypeScript consumers get types without an additional resolve step.
  • publicExports.test.ts:269-277 — pinning that the main entry still exports executeRenderJob, 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:

  1. Keep it module-internal. renderChunk is the one and only legitimate caller; adapters who think they need to pre-apply env can pre-set process.env themselves with plain Node primitives.
  2. Document it as "internal — do not call directly; use renderChunk for 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

@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_refuse_distributed-unsupported_formats_early branch from 60e67b7 to 72407de Compare May 14, 2026 00:14
@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_export_distributed_render_primitives branch from e58413e to 0f646aa Compare May 14, 2026 00:14
vanceingalls
vanceingalls previously approved these changes May 14, 2026
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_refuse_distributed-unsupported_formats_early branch from 72407de to 81975cc Compare May 14, 2026 00:48
miguel-heygen
miguel-heygen previously approved these changes May 14, 2026
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-stamping after rebase. LGTM — no changes from prior review.

@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_refuse_distributed-unsupported_formats_early branch from 81975cc to 5c789ef Compare May 14, 2026 01:13
@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_export_distributed_render_primitives branch from 0f646aa to 1f50bdb Compare May 14, 2026 01:13
@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_refuse_distributed-unsupported_formats_early branch 4 times, most recently from 552027b to e506848 Compare May 14, 2026 03:09
@jrusso1020 jrusso1020 changed the base branch from 05-13-feat_producer_refuse_distributed-unsupported_formats_early to graphite-base/816 May 14, 2026 03:38
@graphite-app
Copy link
Copy Markdown

graphite-app Bot commented May 14, 2026

Merge activity

  • May 14, 3:38 AM UTC: This pull request can not be added to the Graphite merge queue. Please try rebasing and resubmitting to merge when ready.
  • May 14, 3:38 AM UTC: Graphite disabled "merge when ready" on this PR due to: a merge conflict with the target branch; resolve the conflict and try again..

@jrusso1020 jrusso1020 force-pushed the 05-13-feat_producer_export_distributed_render_primitives branch from 1f50bdb to 91a91f6 Compare May 14, 2026 03:41
@jrusso1020 jrusso1020 changed the base branch from graphite-base/816 to main May 14, 2026 03:41
@jrusso1020 jrusso1020 dismissed stale reviews from miguel-heygen and vanceingalls May 14, 2026 03:41

The base branch was changed.

Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-stamping after restack. LGTM.

@jrusso1020 jrusso1020 merged commit 0df6985 into main May 14, 2026
49 of 53 checks passed
@jrusso1020 jrusso1020 deleted the 05-13-feat_producer_export_distributed_render_primitives branch May 14, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants