diff --git a/packages/core/src/beats/beatDetection.test.ts b/packages/core/src/beats/beatDetection.test.ts new file mode 100644 index 0000000000..31bd167535 --- /dev/null +++ b/packages/core/src/beats/beatDetection.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { loadBpmDetective } from "./beatDetection"; + +describe("loadBpmDetective", () => { + it("resolves to null when the import is unavailable", async () => { + const detect = await loadBpmDetective(() => Promise.reject(new Error("missing"))); + expect(detect).toBeNull(); + }); + + it("retries a previously failed import instead of caching null forever", async () => { + let shouldFail = true; + const importFn = async () => { + if (shouldFail) { + throw new Error("transient"); + } + return { default: () => 120 }; + }; + + const first = await loadBpmDetective(importFn); + expect(first).toBeNull(); + + shouldFail = false; + const second = await loadBpmDetective(importFn); + expect(second).not.toBeNull(); + expect(second?.({} as AudioBuffer)).toBe(120); + }); + + it("uses the default export if present, otherwise the module object", async () => { + const importFn = async () => ({ default: () => 90 }); + const detect = await loadBpmDetective(importFn); + expect(detect?.({} as AudioBuffer)).toBe(90); + }); + + it("falls back to the module object when there is no default export", async () => { + const fn = (buffer: AudioBuffer) => buffer.duration; + const importFn = async () => fn as unknown; + const detect = await loadBpmDetective(importFn); + expect(detect).toBe(fn); + }); +}); diff --git a/packages/core/src/beats/beatDetection.ts b/packages/core/src/beats/beatDetection.ts index 2b59b79ad8..59bc109f6b 100644 --- a/packages/core/src/beats/beatDetection.ts +++ b/packages/core/src/beats/beatDetection.ts @@ -4,16 +4,32 @@ // in the browser. type BpmDetect = (buffer: AudioBuffer) => number; let bpmDetectivePromise: Promise | null = null; -function loadBpmDetective(): Promise { - if (!bpmDetectivePromise) { - bpmDetectivePromise = import( - // @ts-ignore -- no type declarations for bpm-detective - "bpm-detective" - ) - .then((m) => ((m as { default?: BpmDetect }).default ?? (m as unknown as BpmDetect)) || null) - .catch(() => null); + +const defaultBpmDetectiveImport = () => + // @ts-ignore -- no type declarations for bpm-detective + import("bpm-detective"); + +export function loadBpmDetective( + importFn: () => Promise = defaultBpmDetectiveImport, +): Promise { + const useCache = importFn === defaultBpmDetectiveImport; + if (useCache && bpmDetectivePromise) { + return bpmDetectivePromise; + } + + const promise = importFn() + .then((m) => ((m as { default?: BpmDetect }).default ?? (m as unknown as BpmDetect)) || null) + .catch(() => { + // Reset the cached promise so a transient import failure can be retried + // on the next call instead of being cached as null for the session. + if (useCache) bpmDetectivePromise = null; + return null; + }); + + if (useCache) { + bpmDetectivePromise = promise; } - return bpmDetectivePromise; + return promise; } const WINDOW_SIZE = 1024;