fix(cli): ignore generated caches in project watcher - #2952
Conversation
vanceingalls
left a comment
There was a problem hiding this comment.
Review — APPROVE
Fix correctly addresses the specific feedback loop identified in #2951: proxy-cache utimesSync → watcher SSE file-change → preview iframe reload → composition re-requests media → repeat. Adding .transcode-cache to WATCHER_EXCLUDED_DIRS breaks the closed loop. The preemptive additions of .thumbnails and .waveform-cache are the same failure mode on adjacent surfaces (waveform generation writes on cache miss; thumbnails are a plausible sibling).
Correctness confirmed.
shouldWatchProjectFilesplits on/and\and checks each path segment against the Set — nested paths likemedia/subdir/.transcode-cache/f.mp4also get excluded. ✓- Set semantics are order-independent for lookup; alphabetical order in the source is a maintainer signal.
- Test asserts all three new dirs return
falseviashouldWatchProjectFile. - Cache dirs verified to exist as described:
packages/studio-server/src/helpers/proxyTranscoder.tswrites to.transcode-cache(CACHE_DIR_NAME) and callsutimesSyncon hits.packages/studio-server/src/routes/waveform.tswrites.waveform-cache..thumbnailsalready excluded insafePath.ts::IGNORE_DIRS, consistent naming.
Non-blocking notes.
-
Alphabetical placement of
.waveform-cache. Inserted between.transcode-cacheand.vite; alphabetically.waveform-cache(w-a) should come after.vite(v). Purely cosmetic — a Set doesn't care — but the surrounding list is alphabetized, so a future reader will bounce off it. One-line reorder if you want to keep the invariant tight. -
Deferred parity risk — second-order UX. PR body correctly flags that
projectSignature.ts::SIGNATURE_EXCLUDED_DIRSandsafePath.ts::IGNORE_DIRSstill omit these dirs, deferred pending dedicated tests. Verified the current state:SIGNATURE_EXCLUDED_DIRS:.cache, .git, .hyperframes, .next, .vite, build, coverage, dist, node_modules, outputs, renders— missing all three.safePath.ts::IGNORE_DIRS:.thumbnails, node_modules, .git— missing.transcode-cache+.waveform-cache.
Practical implication:
useProjectSignaturePollinStoryboardViewpolls every 2 s and firesonChangewhen the project signature shifts.projectSignature.tsfingerprints all files (mtime + size) exceptSIGNATURE_EXCLUDED_DIRS, so.transcode-cache/*.mp4mtime touches frommarkCacheEntryUsed()will still shift the signature — meaning storyboard boards will refetch every 2 s while proxies are actively being served, even after this fix lands.That's a distinct symptom from #2951 (storyboard refetch ≠ preview iframe reload loop, and the preview iframe is fixed by this PR), so it's fair to defer. Worth filing a follow-up so it doesn't get lost — the fix is trivial (add the same three entries to
SIGNATURE_EXCLUDED_DIRS) but needs its own test scaffolding, which is why the deferral makes sense.
Verification.
- Fixes #2951's identified loop at the reported reproduction path.
- CI test evidence in PR body is credible (TDD RED → GREEN cycles per exclusion).
- No behavioral change for legit project files; only three dot-prefixed generated dirs added.
APPROVE at bf739a4db2807373a0c294224dd4a101c6ed8954.
— Via
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Loop-break fix is correct — this closes issue #2951 cleanly. Watcher gate at packages/cli/src/server/fileWatcher.ts:33 (shouldWatchProjectFile) now short-circuits .transcode-cache/* mtime touches from markCacheEntryUsed (packages/studio-server/src/helpers/proxyTranscoder.ts:262, called at :484), which was the loop's only actual driver — .thumbnails and .waveform-cache don't call utimesSync. Adding them anyway is proactive (proxies, thumbnails, waveforms are all generated-dir contents that churn during preview activity, whether via LRU touch or first-write).
Concerns
-
Sibling exclusion lists went un-updated. Three other places carry overlapping cache-dir lists that this PR doesn't touch:
SIGNATURE_EXCLUDED_DIRSatpackages/studio-server/src/helpers/projectSignature.ts:18— missing all three new dirs. Consequence:collectProjectSignatureFilesstill descends into.transcode-cache/.thumbnails/.waveform-cache, so any signature-driven change detection churns as proxies/thumbnails/waveforms get written or LRU-touched. Whether that matters depends on the downstream consumer, which I didn't trace exhaustively — worth checking, or leaving deliberately. (You renamed the test from "excluded from signatures" to just "excluded from" between force-pushes, so you may have already noticed the two-list asymmetry — flagging anyway for the record.)walkFilesinline check atpackages/studio-server/src/routes/files.ts:499-503— has.thumbnailsand.transcode-cachebut not.waveform-cache. Same class of gap.IGNORE_DIRSatpackages/studio-server/src/helpers/safePath.ts:9— only.thumbnails,node_modules,.git.isInHiddenOrVendorDirat line 22 catches the other two viastartsWith("."), so downstream discovery is safe;walkDiritself just does wasted I/O on cache-dir descents.
A shared
GENERATED_DIRSconstant that all four sites imported would remove this class of drift entirely — bigger than a fix-PR scope, but worth an issue.
Nit
- Alphabetical order in the new list:
.waveform-cache(.w) is placed before.vite(.v) atpackages/cli/src/server/fileWatcher.ts:19. Purely aesthetic —Setdoesn't care — but the rest of the list is alphabetical.
Fix itself LGTM.
vanceingalls
left a comment
There was a problem hiding this comment.
R2 — APPROVE at 1d01b9f2c
Both R1 non-blockers addressed cleanly.
R1 Note 1 (alphabetical placement). Fixed — .waveform-cache now sits after .vite in fileWatcher.ts. Same ordering applied to the new entries in projectSignature.ts.
R1 Note 2 (deferred SIGNATURE_EXCLUDED_DIRS + safePath.IGNORE_DIRS parity). Fully closed:
packages/studio-server/src/helpers/projectSignature.ts::SIGNATURE_EXCLUDED_DIRS— added.thumbnails,.transcode-cache,.waveform-cache. This is the signature-poll surface that would have keptStoryboardViewrefetching every 2 s while proxies were being served.packages/studio-server/src/helpers/safePath.ts::IGNORE_DIRS— added.transcode-cache,.waveform-cache(.thumbnailswas already present).walkDirnow hides these from the project file tree.packages/studio-server/src/routes/projects.test.ts— two new tests at the real route boundary, not just unit level:GET /projects/:id/signature: writes files to all three generated dirs, asserts signature unchanged across the three writes, then writessrc/scene.tsand asserts signature does change. RED→GREEN for the signature-poll churn.GET /projects/:id: asserts.transcode-cache/proxy.mp4and.waveform-cache/peaks.jsonare not inpayload.files. Confirms thesafePath.walkDirchange is reachable from the listing route.
Scope decisions I agree with.
- Leaving
routes/files.tsalone: rename-reference scanner, not listing surface. Fine. - Rejecting a shared constant:
fileWatcher.tslives inpackages/cli, the other two live inpackages/studio-server. Different package/layer contracts; cross-package coupling for an incidental overlap would be net-negative. Fine.
Adversarial checks.
- Segment-level match invariant preserved in all three exclusion sites — nested paths like
media/subdir/.transcode-cache/f.mp4are excluded because.split(/[\\/]+/).some(p => set.has(p))fires on the segment, not the full path. SIGNATURE_TEXT_EXTENSIONSunchanged; only the dir filter changed. Non-text files still contribute size/mtime to the fingerprint except under the excluded dirs, which is the correct scope.- No test-only fix: production code changed in both
.tsfiles that carry the deferred parity gap, tests execute through the real routes.
APPROVE at 1d01b9f2cfe44cb7899410e742a9d553c7807e19.
— Via
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Delta 3eb7b1ff → 1d01b9f2c verified at HEAD — all three R1 items resolved.
.waveform-cachesort order — moved to after.viteatpackages/cli/src/server/fileWatcher.ts:19✓SIGNATURE_EXCLUDED_DIRS— three additions atpackages/studio-server/src/helpers/projectSignature.ts:23-26, and the RED→GREEN test atpackages/studio-server/src/routes/projects.test.ts:94-121(ignores generated cache writes while detecting source edits) locks the invariant on both sides: three sequential cache-dir writes leave the signature unchanged, then a real source edit undersrc/moves it. Solid.safePath.walkDir—.transcode-cacheand.waveform-cacheadded toIGNORE_DIRSatpackages/studio-server/src/helpers/safePath.ts:9-15, plus a project-tree test atprojects.test.ts:167-181confirming neither cache file surfaces via/projects/demo.
Verified the routes/files.ts skip: walkFiles at :494 is only consumed by updateReferences at :518, which is the rename-follow scanner ("After a rename, update all references to the old path in project files"), not the file-listing path. Descending into .waveform-cache/peaks.json there would waste I/O but not corrupt anything meaningful — the guard against real corruption is the !content.includes(oldPath) gate at :527 plus the "never bare filenames" comment. Leaving walkFiles alone is defensible.
Shared-constant rejection is also fine — the three lists live in different packages (cli vs studio-server) and encode subtly different contracts (watcher trigger vs signature invalidation vs discovery traversal); coupling them across package boundaries would trade drift for over-constraint. Local + tested is the right pick here.
CI showed one Smoke: global install failure at the time of review (job 91526931643) — that surface is npx-shim smoke, untouched by this PR's directory-list changes; likely pre-existing flake, worth a quick re-run to confirm.
LGTM from my side.
…nerated-cache-watcher fix(cli): ignore generated caches in project watcher
Summary
.transcode-cache,.thumbnails, and.waveform-cachein the recursive project watcherGET /projects/:idfile listings (.thumbnailswas already omitted)Fixes #2951
Root cause and scope
Generated cache paths crossed multiple project-change boundaries. Watcher events could become Studio file-change invalidations, and generated cache writes also changed the signature polled by Storyboard every two seconds. The project listing additionally exposed transcode and waveform cache files.
Static waveform proof:
packages/studio-server/src/routes/waveform.ts:18-39creates and writes.waveform-cachepackages/studio/src/hooks/useRenderClipContent.ts:63-69builds the timeline waveform routepackages/studio/src/player/components/AudioWaveform.tsx:94-100fetches itroutes/files.tsis a separate rename-reference scanner and remains unchanged because no concrete corruption RED was established. No shared generated-directory constant was added because watcher, signature, and listing policies are distinct contracts across package boundaries.TDD evidence
Cycle 1: proxy and thumbnail watcher caches
RED before production:
The investigation independently observed real watcher callbacks after 301 ms for both
.transcode-cache/proxy.mp4and.thumbnails/frame.jpgmtime touches.GREEN after adding only those exclusions: 3/3 focused watcher tests passed.
Cycle 2: waveform watcher cache
At the committed cycle-1 production state, the test-only assertion produced RED:
GREEN after adding only
.waveform-cache: 3/3 focused watcher tests passed.Real watcher control:
{"transcodeEvents":[],"thumbnailEvents":[],"waveformEvents":[],"sourceEvents":["src/scene.ts"]}Cycle 3: test naming review
Renamed the watcher test to
skips generated and dependency directories; no production behavior changed. Focused watcher tests remained 3/3 GREEN.Cycle 4: signature and project listing
Route-level tests were added first on commit
3eb7b1ffd.RED before production edits:
The signature test collected distinct signatures after each generated write, while the listing test exercised the actual route boundary. Only after that RED:
.thumbnails,.transcode-cache, and.waveform-cachewere added toSIGNATURE_EXCLUDED_DIRS.transcode-cacheand.waveform-cachewere added to thesafePath.tsfile-tree ignore set.waveform-cachewas moved after.vitein the watcher list (ordering only)GREEN:
Independent real-filesystem signature control:
{ "initial":"1af301bcc1150fd8a80f883d", "generatedSignatures":{ ".transcode-cache":"1af301bcc1150fd8a80f883d", ".thumbnails":"1af301bcc1150fd8a80f883d", ".waveform-cache":"1af301bcc1150fd8a80f883d" }, "sourceSignature":"c335cd75a57722379cf40281" }All generated writes preserve the signature; a real
src/scene.tswrite changes it.Verification