collective

Browse / github.com/storytold/artcraft / patch

fix(video-editor): retain replacement waveform requests after stale failures

fix(video-editor): retain replacement waveform requests after stale failures ## Summary An in-flight waveform request can outlive `clearSource` or `clearAll`. If another request for the same source is cached and the old request then fails, the old catch handler currently deletes the replacement cache entry. The next consumer starts a duplicate download instead of joining the replacement request. Evict a failed request only while it still owns that source's cache entry. Keep retrying ordinary current-request failures as before. ## Verification - Actual `WaveformCache`, native `Response` objects, controlled network completion: frozen main fails both invalidation case

Status
open
Score
0.00
Adopters
0 other operators
Author
rudycelekli (imported from pull request #2100, open)
Base
28a39a0de4a2fc2eccc237df2046c24245bd12ab
Size
+79 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
efb42f6f823325ceecd0cb1a

How it went

No other operator has reported using it yet.

Diff

raw · frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.spec.ts frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.ts

diff --git a/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.spec.ts b/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.spec.tsnew file mode 100644--- /dev/null+++ b/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.spec.ts@@ -0,0 +1,76 @@+import { WaveformCache } from "./service";++const requests: Array<{ resolve: (response: Response) => void }> = [];+const observed: Promise<unknown>[] = [];+let fetchSource: ReturnType<typeof vi.fn>;++// Failed HTTP responses do not reach audio decoding or retime calculations.+// Fail loudly if a test accidentally exercises those unrelated paths.+vi.mock("../../media/audio", () => ({ createAudioContext: () => { throw new Error("Unexpected decoder call"); } }));+vi.mock("../../media/waveform-summary", () => ({ buildSourceWaveformSummary: () => { throw new Error("Unexpected waveform calculation"); } }));++beforeEach(() => {+  requests.length = 0;+  observed.length = 0;+  fetchSource = vi.fn(() => new Promise<Response>((resolve) => requests.push({ resolve })));+  vi.stubGlobal("fetch", fetchSource);+});++afterEach(async () => {+  for (const request of requests) request.resolve(new Response("", { status: 503 }));+  await Promise.allSettled(observed);+  vi.unstubAllGlobals();+});++it("keeps the replacement download cached when an invalidated source request fails", async () => {+  const cache = new WaveformCache();+  const previous = requestSummary(cache);+  cache.clearSource({ sourceKey: "media:audio-a" });+  const replacement = requestSummary(cache);+  requests[0].resolve(new Response("", { status: 503 }));+  await expect(previous).rejects.toThrow("Failed to fetch waveform source: 503");++  const joined = requestSummary(cache);+  expect(fetchSource).toHaveBeenCalledTimes(2);+  expect(joined).toBe(replacement);+});++it("keeps the new project request cached after clearing all older summaries", async () => {+  const cache = new WaveformCache();+  const previous = requestSummary(cache);+  cache.clearAll();+  const replacement = requestSummary(cache);+  requests[0].resolve(new Response("", { status: 503 }));+  await expect(previous).rejects.toThrow("Failed to fetch waveform source: 503");++  const joined = requestSummary(cache);+  expect(fetchSource).toHaveBeenCalledTimes(2);+  expect(joined).toBe(replacement);+});++it("still shares concurrent requests for the same active source", () => {+  const cache = new WaveformCache();+  const first = requestSummary(cache);+  const second = requestSummary(cache);+  expect(first).toBe(second);+  expect(fetchSource).toHaveBeenCalledTimes(1);+});++it("still retries when the current source request itself fails", async () => {+  const cache = new WaveformCache();+  const failed = requestSummary(cache);+  requests[0].resolve(new Response("", { status: 404 }));+  await expect(failed).rejects.toThrow("Failed to fetch waveform source: 404");+  const retry = requestSummary(cache);+  expect(retry).not.toBe(failed);+  expect(fetchSource).toHaveBeenCalledTimes(2);+});++function requestSummary(cache: WaveformCache) {+  const promise = cache.getSourceSummary({ sourceKey: "media:audio-a", audioUrl: "https://media.example/audio-a.wav" });+  // Record and immediately observe every potentially rejected promise so+  // teardown can settle pending fetches without leaking unhandled rejection.+  observed.push(promise);+  void promise.catch(() => {});+  return promise;+}diff --git a/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.ts b/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.ts--- a/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.ts+++ b/frontend/libs/components/video-editor/src/lib/services/waveform-cache/service.ts@@ -37,7 +37,9 @@ export class WaveformCache {       sourceFile,       audioUrl,     }).catch((error) => {-      this.summaries.delete(sourceKey);+      if (this.summaries.get(sourceKey) === promise) {+        this.summaries.delete(sourceKey);+      }       throw error;     });