diff --git a/src/three-components/StepModel.tsx b/src/three-components/StepModel.tsx index f84ddc0f..5a70396e 100644 --- a/src/three-components/StepModel.tsx +++ b/src/three-components/StepModel.tsx @@ -10,6 +10,7 @@ import { import { GLTFExporter } from "three-stdlib" import { GltfModel } from "./GltfModel" import type { CadModelFitMode, CadModelSize } from "src/utils/cad-model-fit" +import { getStepModelCacheKey } from "src/utils/step-model-cache-key" type OcctImportParams = { linearUnit?: "millimeter" | "centimeter" | "meter" | "inch" | "foot" @@ -163,9 +164,9 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer { return bytes.buffer } -function getCachedGlb(stepUrl: string): ArrayBuffer | null { +function getCachedGlb(cacheKey: string): ArrayBuffer | null { try { - const cached = localStorage.getItem(`${CACHE_PREFIX}${stepUrl}`) + const cached = localStorage.getItem(`${CACHE_PREFIX}${cacheKey}`) if (!cached) { return null } @@ -176,10 +177,10 @@ function getCachedGlb(stepUrl: string): ArrayBuffer | null { } } -function setCachedGlb(stepUrl: string, glb: ArrayBuffer): void { +function setCachedGlb(cacheKey: string, glb: ArrayBuffer): void { try { const encoded = arrayBufferToBase64(glb) - localStorage.setItem(`${CACHE_PREFIX}${stepUrl}`, encoded) + localStorage.setItem(`${CACHE_PREFIX}${cacheKey}`, encoded) } catch (error) { console.warn("Failed to write STEP GLB cache", error) } @@ -246,7 +247,8 @@ export const StepModel = ({ let objectUrl: string | null = null let shouldRevokeObjectUrl = true const registry = getStepUrlConversionRegistry() - const cachedGlb = getCachedGlb(stepUrl) + const stepCacheKey = getStepModelCacheKey(stepUrl) + const cachedGlb = getCachedGlb(stepCacheKey) if (cachedGlb) { const cachedConverted: ConvertedStepFile = { arrayBuffer: cachedGlb, @@ -254,7 +256,7 @@ export const StepModel = ({ new Blob([cachedGlb], { type: "model/gltf-binary" }), ), } - registry.completed.set(stepUrl, cachedConverted) + registry.completed.set(stepCacheKey, cachedConverted) objectUrl = cachedConverted.blobUrl shouldRevokeObjectUrl = false setStepGltfUrl(cachedConverted.blobUrl) @@ -265,12 +267,12 @@ export const StepModel = ({ } } } - const existingCompleted = registry.completed.get(stepUrl) + const existingCompleted = registry.completed.get(stepCacheKey) if (existingCompleted) { objectUrl = existingCompleted.blobUrl shouldRevokeObjectUrl = false setStepGltfUrl(existingCompleted.blobUrl) - setCachedGlb(stepUrl, existingCompleted.arrayBuffer) + setCachedGlb(stepCacheKey, existingCompleted.arrayBuffer) return () => { isActive = false if (objectUrl && shouldRevokeObjectUrl) { @@ -278,7 +280,7 @@ export const StepModel = ({ } } } - let conversionPromise = registry.inProgress.get(stepUrl) + let conversionPromise = registry.inProgress.get(stepCacheKey) if (!conversionPromise) { conversionPromise = convertStepUrlToGlb(stepUrl) .then((glbBuffer) => { @@ -288,15 +290,15 @@ export const StepModel = ({ new Blob([glbBuffer], { type: "model/gltf-binary" }), ), } - registry.completed.set(stepUrl, converted) - registry.inProgress.delete(stepUrl) + registry.completed.set(stepCacheKey, converted) + registry.inProgress.delete(stepCacheKey) return converted }) .catch((error) => { - registry.inProgress.delete(stepUrl) + registry.inProgress.delete(stepCacheKey) throw error }) - registry.inProgress.set(stepUrl, conversionPromise) + registry.inProgress.set(stepCacheKey, conversionPromise) } void conversionPromise .then((converted) => { @@ -306,7 +308,7 @@ export const StepModel = ({ objectUrl = converted.blobUrl shouldRevokeObjectUrl = false setStepGltfUrl(converted.blobUrl) - setCachedGlb(stepUrl, converted.arrayBuffer) + setCachedGlb(stepCacheKey, converted.arrayBuffer) }) .catch((error) => { console.error("Failed to convert STEP file to GLB", error) diff --git a/src/utils/step-model-cache-key.ts b/src/utils/step-model-cache-key.ts new file mode 100644 index 00000000..7f1e0885 --- /dev/null +++ b/src/utils/step-model-cache-key.ts @@ -0,0 +1,24 @@ +export function getStepModelCacheKey(stepUrl: string): string { + const hashIndex = stepUrl.indexOf("#") + const urlWithoutHash = + hashIndex === -1 ? stepUrl : stepUrl.slice(0, hashIndex) + const hash = hashIndex === -1 ? "" : stepUrl.slice(hashIndex) + const queryIndex = urlWithoutHash.indexOf("?") + + if (queryIndex === -1) { + return stepUrl + } + + const path = urlWithoutHash.slice(0, queryIndex) + const query = urlWithoutHash.slice(queryIndex + 1) + const filteredQuery = query + .split("&") + .filter((part) => part.split("=", 1)[0] !== "cachebust_origin") + .join("&") + + if (!filteredQuery) { + return `${path}${hash}` + } + + return `${path}?${filteredQuery}${hash}` +} diff --git a/tests/step-model-cache-key.test.ts b/tests/step-model-cache-key.test.ts new file mode 100644 index 00000000..fb9538f5 --- /dev/null +++ b/tests/step-model-cache-key.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "bun:test" +import { getStepModelCacheKey } from "../src/utils/step-model-cache-key" + +test("normalizes empty cachebust_origin query params for STEP cache keys", () => { + expect( + getStepModelCacheKey( + "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=abc&pn=C1&cachebust_origin=", + ), + ).toBe( + "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=abc&pn=C1", + ) +}) + +test("normalizes cachebust_origin wherever it appears in the query", () => { + expect( + getStepModelCacheKey( + "/models/chip.step?cachebust_origin=preview-1&uuid=abc&pn=C1#part", + ), + ).toBe("/models/chip.step?uuid=abc&pn=C1#part") + + expect( + getStepModelCacheKey( + "/models/chip.step?uuid=abc&cachebust_origin=preview-2&pn=C1", + ), + ).toBe("/models/chip.step?uuid=abc&pn=C1") +}) + +test("preserves meaningful query params and hashes for distinct STEP URLs", () => { + expect(getStepModelCacheKey("/models/chip.step?variant=A#pins")).toBe( + "/models/chip.step?variant=A#pins", + ) + expect(getStepModelCacheKey("/models/chip.step#pins")).toBe( + "/models/chip.step#pins", + ) +})