refactor(producer): move shared render helpers to render/shared.ts#720
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
The right call to break the circular import now rather than letting it grow through 7 more stage PRs. The re-export from renderOrchestrator.ts preserves backward compatibility for external callers — zero churn for test files.
The 5 moved items (CompositionMetadata, applyRenderModeHints, resolveDeviceScaleFactor, writeCompiledArtifacts, projectBrowserEndToCompositionTimeline) are pure functions / types with no dependency on the orchestrator's runtime state, so the extraction is clean.
Good that compileStage.ts and probeStage.ts switch to importing from `../shared.js` while keeping `import type { RenderJob } from "../../renderOrchestrator.js"` — type-only imports are erased at runtime and don't form cycles.
LGTM — nice clean stack overall.
vanceingalls
left a comment
There was a problem hiding this comment.
Verdict: Approve.
Right call to land this cycle-break now, before PRs 1.4-1.10 each add a back-edge to renderOrchestrator.ts. The /simplify flag was correct.
I verified every helper moved to render/shared.ts is used by ≥2 sites: applyRenderModeHints, resolveDeviceScaleFactor, writeCompiledArtifacts, projectBrowserEndToCompositionTimeline — all consumed by both compileStage.ts and probeStage.ts (and writeCompiledArtifacts additionally by the back-compat re-export from renderOrchestrator.ts). Nothing is moved on a hypothesis of future use. CompositionMetadata is similarly consumed by both stages.
The back-compat re-export at renderOrchestrator.ts:546 correctly keeps renderOrchestrator.test.ts working without churn — I checked the test file and it imports all five symbols from ./renderOrchestrator.js. No external consumer breaks.
The body of each moved helper is byte-identical to the original. I diffed writeCompiledArtifacts, resolveDeviceScaleFactor, applyRenderModeHints, and projectBrowserEndToCompositionTimeline line-by-line against main — comments, JSDoc, error messages, the cross-multiplication aspect-ratio check, the isPathInside Windows fix (GH #321), the includeSummary summary.json payload shape — all preserved.
compileStage.ts and probeStage.ts switch their runtime imports from ../../renderOrchestrator.js to ../shared.js for the four helpers + CompositionMetadata, and keep import type { RenderJob } from the orchestrator. Type-only imports are erased at runtime — no cycle. Correctly handled.
Important
-
(important)
renderOrchestrator.ts:546-552— the back-compat re-export is correct behavior-wise, but it doubles the public surface for these symbols (./render/shared.jsAND./renderOrchestrator.js). New code in the stages directory imports fromshared.js, butrenderOrchestrator.test.tsand any future producer-internal caller could pick either path. Suggest one of:- Add a JSDoc
@deprecated Re-exported from ./render/shared.js for back-compat; new imports should use shared.js directly.to the re-export block, plus an oxlint rule for the producer package to flag new imports fromrenderOrchestrator.tsfor these symbols. - Migrate
renderOrchestrator.test.tsto import from./render/shared.jsdirectly in this PR (zero behavior change, drops the re-export entirely).
Option 2 is the right end-state. Option 1 is a fine intermediate step if you want to keep this PR small. Not blocking, but please don't leave the re-export hanging indefinitely — back-compat shims that outlive their purpose are how trees turn into rainforests.
- Add a JSDoc
Nits
- (nit)
shared.tsis well-named for what it currently holds (four helpers + one interface), but if it ends up accreting > ~10 symbols across the next six PRs, consider splitting (shared/compositionMetadata.ts,shared/scaleFactor.ts, etc.). Not yet — premature for 196 lines. - (nit)
BROWSER_MEDIA_EPSILONis still duplicated inprobeStage.ts:132. If a future stage also reconciles browser media (chunked rendering will likely re-probe), hoist it toshared.ts. Easy follow-up. - (nit)
shared.tsheader comment mentions "the orchestrator imports the stage functions" — true today, but if you ever flip the dependency direction so stages own their own composition, this comment will mislead. Phrase it as "to keep stages free of runtime imports from the orchestrator" instead.
Praise
- The proactive cycle-break is correct engineering judgment. The cycle worked because module-init order happened to resolve it, but TDZ-on-future-top-level-access was a real latent risk. Fixing it after one stage extraction (not seven) is exactly the right time.
- Removing the now-orphaned
CANVAS_DIMENSIONS,VideoElement,AudioElement,ImageElementimports from the orchestrator is good housekeeping. - The PSNR/audio-correlation parity check (1.000 / 0.994 / 0.975, matching #719 exactly) is strong evidence of "moved, not changed".
Stack-coherence summary across #717-#720: the stage interface (typed input object → typed async result object, resources returned to sequencer for cleanup, no orchestrator imports at runtime) is settling into a clean contract. After this PR, a chunkStage would plug in cleanly with one caveat — the cfg.forceScreenshot mutation in compileStage (flagged on #718) needs to move into the result object before the freeze step can be replay-safe. Otherwise the foundation is good.
— Vai
Breaks the runtime circular import between `renderOrchestrator.ts` and
the stage files under `services/render/stages/`. Before this change the
stages imported runtime helpers (`writeCompiledArtifacts`,
`applyRenderModeHints`, `resolveDeviceScaleFactor`,
`projectBrowserEndToCompositionTimeline`) and types (`CompositionMetadata`)
back from `renderOrchestrator.ts`, which itself imports the stage
functions. The cycle resolved at build time because both modules
finished initializing before any stage was invoked, but it was fragile
and would keep growing as more stages were extracted.
This PR:
- Adds `packages/producer/src/services/render/shared.ts` and moves the
four functions plus the `CompositionMetadata` interface into it.
- Has `renderOrchestrator.ts` re-export everything from `shared.ts`, so
external callers (the existing `renderOrchestrator.test.ts`, any code
importing `applyRenderModeHints` etc. from the orchestrator) keep
working with no churn on their side.
- Updates `compileStage.ts` and `probeStage.ts` to import the runtime
helpers from `../shared.js`. The only remaining import from
`renderOrchestrator.ts` in the stages is `import type { RenderJob }`,
which is erased at runtime and creates no cycle.
- Removes the imports the orchestrator no longer needs after losing
the four function definitions (`CANVAS_DIMENSIONS`, `VideoElement`,
`AudioElement`, `ImageElement`).
No behavior change. Renderer smoke-tested inside `Dockerfile.test`
against `font-variant-numeric`, `many-cuts`, and `variables-prod` —
all PSNR / audio-correlation baselines match PR 1.3 exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-export Three small follow-ups on the shared.ts extraction, addressing review feedback on #720: - Hoist `BROWSER_MEDIA_EPSILON` from `probeStage.ts` into `shared.ts` so any future stage that reconciles browser media (chunked rendering re-probe, for instance) doesn't have to redeclare it. - Migrate `renderOrchestrator.test.ts` to import the five moved symbols (`applyRenderModeHints`, `projectBrowserEndToCompositionTimeline`, `resolveDeviceScaleFactor`, `writeCompiledArtifacts`, `CompositionMetadata`) directly from `./render/shared.js`. This is the clean end state — the back-compat re-export through `renderOrchestrator.ts` was a stepping-stone. - Drop the back-compat re-export block from `renderOrchestrator.ts`. No remaining importers go through it (verified via grep across `packages/`). The five symbols now have exactly one path: `./render/shared.js`. No behavior change. Renderer smoke-tested inside `Dockerfile.test` against `font-variant-numeric`, `many-cuts`, and `variables-prod` — audio correlations 1.000 / 0.994 / 0.975, matching every prior PR in the stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2925f24 to
89d83fb
Compare
835c834 to
2024251
Compare
|
Thanks @vanceingalls @miguel-heygen — addressed all three items in commit
Renderer smoke-tested inside Stack-coherence note: the |
The base branch was changed.
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-approving after new commits. Same verdict — clean cycle-break, back-compat re-exports correct, helpers byte-identical to originals. LGTM.
vanceingalls
left a comment
There was a problem hiding this comment.
Re-stamp at 89d83fbb.
My prior approval (pullrequestreview-4266433387 on 2925f24e) was dismissed by the branch protection rule after the new push. Verified the new commits address my prior important follow-up:
89d83fbb— "migrate test imports to render/shared.ts, drop re-export" — exactly what was asked for. The back-compat re-export shim I flagged ("the only internal consumer was a test file — don't let the shim outlive its purpose") is now removed, with the test file updated to import directly fromrender/shared.ts. Clean closure on the import path.- Stack rebase on top of the now-landed #717-#719 pulled the stage scaffolding into #720's diff (shared.ts +204, stages/compileStage.ts +121, stages/probeStage.ts +369, renderOrchestrator.ts -495). All scoped to the moved helpers + stage extractions from the parent PRs — no new logic introduced in this PR.
— Vai

What
Breaks the runtime circular import between `renderOrchestrator.ts` and the stage files under `services/render/stages/`. Adds `packages/producer/src/services/render/shared.ts` and moves four small helpers plus one interface into it:
`renderOrchestrator.ts` re-exports everything from `shared.ts`, so external callers (the existing `renderOrchestrator.test.ts` and any other code importing these symbols from the orchestrator) keep working with zero churn.
Why
Before this PR, the stages imported runtime helpers back from `renderOrchestrator.ts`, which itself imports the stage functions. The cycle resolved at build time because both modules finished initializing before any stage was invoked, but it was fragile (any future top-level access to those symbols during module init would TDZ) and would keep growing as more stages were extracted. By PR 1.10 the cycle would be ~8 functions deep.
Land this small intermediate PR now so PRs 1.4–1.10 import from `shared.ts` instead of `renderOrchestrator.ts` and the cycle never grows.
A `/simplify` review on the stack flagged this as the highest-confidence "worth considering" item; the call here is to fix the cycle now rather than ship 7 more stage PRs with a steadily-growing back-edge.
How
Preserved invariants
Test plan
🤖 Generated with Claude Code