From b7ab4c3174a081ad169b3fd45921c6a5f904eea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=E1=BA=A1i=20Qu=C3=BD?= Date: Tue, 12 May 2026 21:58:04 +0700 Subject: [PATCH 1/3] Normalize OBJ loader cache keys by stripping cachebust_origin Co-authored-by: Cursor --- src/hooks/use-global-obj-loader.ts | 19 ++++++++++--------- src/utils/normalize-obj-cache-url.ts | 18 ++++++++++++++++++ tests/normalize-obj-cache-url.test.ts | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 src/utils/normalize-obj-cache-url.ts create mode 100644 tests/normalize-obj-cache-url.test.ts diff --git a/src/hooks/use-global-obj-loader.ts b/src/hooks/use-global-obj-loader.ts index 69422095..8a35a515 100644 --- a/src/hooks/use-global-obj-loader.ts +++ b/src/hooks/use-global-obj-loader.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from "react" import type { Object3D } from "three" import { MTLLoader, OBJLoader } from "three-stdlib" +import { normalizeObjCacheUrl } from "src/utils/normalize-obj-cache-url" import { loadVrml } from "src/utils/vrml" // Define the type for our cache @@ -28,21 +29,21 @@ export function useGlobalObjLoader( useEffect(() => { if (!url) return - const cleanUrl = url.replace(/&cachebust_origin=$/, "") + const cacheKey = normalizeObjCacheUrl(url) const cache = window.TSCIRCUIT_OBJ_LOADER_CACHE let hasUrlChanged = false async function loadAndParseObj() { try { - if (cleanUrl.endsWith(".wrl")) { - return await loadVrml(cleanUrl) + if (cacheKey.endsWith(".wrl")) { + return await loadVrml(url) } - const response = await fetch(cleanUrl) + const response = await fetch(url) if (!response.ok) { throw new Error( - `Failed to fetch "${cleanUrl}": ${response.status} ${response.statusText}`, + `Failed to fetch "${url}": ${response.status} ${response.statusText}`, ) } const text = await response.text() @@ -79,8 +80,8 @@ export function useGlobalObjLoader( } function loadUrl() { - if (cache.has(cleanUrl)) { - const cacheItem = cache.get(cleanUrl)! + if (cache.has(cacheKey)) { + const cacheItem = cache.get(cacheKey)! if (cacheItem.result) { // If we have a result, clone it return Promise.resolve(cacheItem.result.clone()) @@ -97,10 +98,10 @@ export function useGlobalObjLoader( // If the result is an Error, return it return result } - cache.set(cleanUrl, { ...cache.get(cleanUrl)!, result }) + cache.set(cacheKey, { ...cache.get(cacheKey)!, result }) return result }) - cache.set(cleanUrl, { promise, result: null }) + cache.set(cacheKey, { promise, result: null }) return promise } diff --git a/src/utils/normalize-obj-cache-url.ts b/src/utils/normalize-obj-cache-url.ts new file mode 100644 index 00000000..daebe069 --- /dev/null +++ b/src/utils/normalize-obj-cache-url.ts @@ -0,0 +1,18 @@ +/** + * Returns a stable key for OBJ/WRL model fetch+parse caching. + * EasyEDA CDN URLs often differ only by `cachebust_origin=...`, which should not + * split the cache (same bytes, redundant loads). + */ +export function normalizeObjCacheUrl(url: string): string { + const trimmed = url.replace(/&cachebust_origin=$/, "") + try { + const parsed = new URL(trimmed) + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + parsed.searchParams.delete("cachebust_origin") + return parsed.toString() + } + } catch { + // non-absolute URL or invalid + } + return trimmed +} diff --git a/tests/normalize-obj-cache-url.test.ts b/tests/normalize-obj-cache-url.test.ts new file mode 100644 index 00000000..480ab266 --- /dev/null +++ b/tests/normalize-obj-cache-url.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { normalizeObjCacheUrl } from "../src/utils/normalize-obj-cache-url" + +test("strips cachebust_origin for identical CDN identity", () => { + const a = + "https://modelcdn.example/easyeda_models/download?uuid=abc&pn=C1&cachebust_origin=https%3A%2F%2Fa.com" + const b = + "https://modelcdn.example/easyeda_models/download?uuid=abc&pn=C1&cachebust_origin=https%3A%2F%2Fb.com" + expect(normalizeObjCacheUrl(a)).toBe(normalizeObjCacheUrl(b)) +}) + +test("legacy empty cachebust suffix at end of query", () => { + const u = + "https://modelcdn.example/easyeda_models/download?uuid=abc&cachebust_origin=" + expect(normalizeObjCacheUrl(u)).toBe( + "https://modelcdn.example/easyeda_models/download?uuid=abc", + ) +}) From bafc08f83ddf0d0692fe07f5443120097d3734fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=E1=BA=A1i=20Qu=C3=BD?= Date: Tue, 12 May 2026 22:02:15 +0700 Subject: [PATCH 2/3] Fix TS narrow for fetch URL in useGlobalObjLoader Co-authored-by: Cursor --- src/hooks/use-global-obj-loader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/use-global-obj-loader.ts b/src/hooks/use-global-obj-loader.ts index 8a35a515..84c1a75c 100644 --- a/src/hooks/use-global-obj-loader.ts +++ b/src/hooks/use-global-obj-loader.ts @@ -28,8 +28,8 @@ export function useGlobalObjLoader( useEffect(() => { if (!url) return - - const cacheKey = normalizeObjCacheUrl(url) + const fetchUrl = url + const cacheKey = normalizeObjCacheUrl(fetchUrl) const cache = window.TSCIRCUIT_OBJ_LOADER_CACHE let hasUrlChanged = false @@ -37,13 +37,13 @@ export function useGlobalObjLoader( async function loadAndParseObj() { try { if (cacheKey.endsWith(".wrl")) { - return await loadVrml(url) + return await loadVrml(fetchUrl) } - const response = await fetch(url) + const response = await fetch(fetchUrl) if (!response.ok) { throw new Error( - `Failed to fetch "${url}": ${response.status} ${response.statusText}`, + `Failed to fetch "${fetchUrl}": ${response.status} ${response.statusText}`, ) } const text = await response.text() From 3d8368881d58e3b569f980cae9b4a5144c1d9717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=E1=BA=A1i=20Qu=C3=BD?= <105071667+daiquydev@users.noreply.github.com> Date: Tue, 12 May 2026 23:17:38 +0700 Subject: [PATCH 3/3] fix: resolve typescript nullability error in use-global-obj-loader --- src/hooks/use-global-obj-loader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/use-global-obj-loader.ts b/src/hooks/use-global-obj-loader.ts index 84c1a75c..847d85e9 100644 --- a/src/hooks/use-global-obj-loader.ts +++ b/src/hooks/use-global-obj-loader.ts @@ -28,8 +28,8 @@ export function useGlobalObjLoader( useEffect(() => { if (!url) return - const fetchUrl = url - const cacheKey = normalizeObjCacheUrl(fetchUrl) + const currentUrl = url + const cacheKey = normalizeObjCacheUrl(currentUrl) const cache = window.TSCIRCUIT_OBJ_LOADER_CACHE let hasUrlChanged = false @@ -37,13 +37,13 @@ export function useGlobalObjLoader( async function loadAndParseObj() { try { if (cacheKey.endsWith(".wrl")) { - return await loadVrml(fetchUrl) + return await loadVrml(currentUrl) } - const response = await fetch(fetchUrl) + const response = await fetch(currentUrl) if (!response.ok) { throw new Error( - `Failed to fetch "${fetchUrl}": ${response.status} ${response.statusText}`, + `Failed to fetch "${currentUrl}": ${response.status} ${response.statusText}`, ) } const text = await response.text()