From f9f4c99e4f7fa1ca4c5bcca55f770133ded53e0d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 10 Nov 2026 20:22:36 -0500 Subject: [PATCH] frontend: Sonar reliability fixes (promises in conditions, async executors, identical branches, reduce, regex) (#2821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears 51 open TypeScript issues. Typecheck and build are left to CI. **typescript:S6544 (17)** - A cached promise checked by truthiness (`if (existing)`, `if (cached)`, `if (ammoLoadPromise)`…) is checked as `!== undefined` / `!== null` against its map or field type. Same branches, since a Promise is always truthy. - `scene.ts` `load_glb_wrapped_no_cors` / `load_splat_wrapped_no_cors`: the fetch moves out of the `async` Promise executor. The methods were already `async`, so a failed fetch now `throw`s (rejects the same way), and only the callback APIs (`GLTFLoader.parse`, `SplatMesh` `onLoad`) stay inside `new Promise`. - `SceneManagerAPI` declares the promises its implementation returns (`create`/`retrieve` → `Promise`, `update`/`delete` → `Promise`). Nothing calls through the interface. - `convertFbxToGlb.worker.ts`: `onmessage` calls `void convert(event)`. **typescript:S3923 / S1764 / S3516 / S905 (12)**: identical branches collapsed (`variant === "world" ? "max-w-[1600px]" : "max-w-[1600px]"`, `ScissorIcon : ScissorIcon`, `index % 2 === 0 ? point + offset : point + offset`, PromptBox3D's two `setIsEnqueueing(false); return;` arms, `{DEFAULT_GRID_CONFIG ? null : null}` and its now-unused import), `x && x`, the `this.gridHelper;` statement, and `CurrentUserBadge`, which always rendered `null` (its markup was commented out). **typescript:S6959 (5)**: each `reduce` starts from the first element. Every one is behind an emptiness check, and comparing the first element with itself keeps it. **typescript:S2871 (4)**: `sort((a, b) => a.localeCompare(b))` for the order-independent cache keys and the board and font name lists. **typescript:S8786 (8)** - Markdown link text is `[^\][]+`. `@image(url, size)` drops the `\s*` before the size (it's `.trim()`ed already). ATX headings `trimStart()` the text instead of `\s*`. Slugs trim one `-` (runs were already collapsed to one). - `color.ts` `stripCssNoise`: `!important` is replaced by a space, then trailing `;` are trimmed in a loop. - Data-URL mime: `/:([^;]*);/`. - `ControlPanelSceneObject` number check: `/^-?\d*(\.\d*)?$/`. The old `.` was unescaped and matched any character, so `12a3` passed as a number. **Others**: `window.open(…, "noopener,noreferrer")` (S5148). `AlertTitle` renders `children` explicitly (S6850). The folder name hash wraps with `Math.imul(…, 1)` instead of `| 0`, the same int32 result (S7767). `autoFocus` is removed from the two rename inputs, which their refs already focus and select (S9379). **Not here**: S7059 in `editor.ts` (`ensureAmmoLoaded()` in the constructor) is on a line #2807 changes. ## Summary by CodeRabbit * **Bug Fixes** * Improved reliability when loading 3D scene assets and completing drag-and-drop operations. * Corrected numeric input validation, scene import handling, and video duration tie handling. * Improved media, audio, and video cache handling to avoid unnecessary retries or missed waits. * Refined markdown parsing for links, headings, image directives, and heading anchors. * Discord invites now open with additional browser protections. * Improved CSS color cleanup and handling of pasted scene elements. * **Improvements** * Made ordering of tags, fonts, and prompt-related items more consistent. * Improved video editor timeline selections. * Removed the non-functional account badge from the video editor header. **typescript:S1848 (2)**: `new SplatMesh({ …, onLoad })` created an instance only for its callback. `scene.ts` now awaits `mesh.initialized` (Spark's promise, which resolves after load), so a failed load rejects instead of leaving the promise pending. `viewer-3d.tsx` runs the same body in `initialized.then`, with a `.catch` that logs. Kept as is: `TabState.setActiveTab` still resolves `true` (S3516 stays open), because `TabState.spec.ts` and the tab-change benchmark read its result. --- .../experiences/ImageTo3DExperience.tsx | 4 +- .../src/lib/button-dropdown.tsx | 2 +- .../src/lib/FolderNameDialog.tsx | 1 - .../gallery-modal/src/lib/gallery-modal.tsx | 3 +- .../src/lib/media-prompt-token-cache.ts | 2 +- .../generation-list/src/lib/prompts-cache.ts | 2 +- .../login-modal/src/lib/login-modal.tsx | 2 +- .../src/lib/boards/boardSelectors.ts | 2 +- .../pagedraw/src/lib/stores/SceneState.ts | 6 +- .../pagescene/src/lib/DragAndDrop/DndAsset.ts | 2 +- .../ControlPanelSceneObject.tsx | 2 +- .../pagescene/src/lib/engine/ammoLoader.ts | 2 +- .../pagescene/src/lib/engine/scene.ts | 67 ++++++++----------- .../src/lib/engine/scene_manager_api.ts | 12 ++-- .../promptbox/src/lib/PromptBox3D.tsx | 10 +-- .../utilities/convertFbxToGlb.worker.ts | 6 +- .../lib/commands/timeline/clipboard/paste.ts | 6 +- .../lib/components/editor/editor-header.tsx | 17 ----- .../src/lib/components/ui/alert.tsx | 6 +- .../src/lib/core/managers/audio-manager.ts | 4 +- .../src/lib/fonts/google-fonts.ts | 2 +- .../src/lib/fonts/use-font-atlas.ts | 4 +- .../src/lib/guides/definitions/grid.tsx | 4 -- .../src/lib/preview/preview-snap.ts | 8 ++- .../lib/services/renderer/nodes/image-node.ts | 2 +- .../services/renderer/nodes/sticker-node.ts | 2 +- .../src/lib/services/video-cache/service.ts | 4 +- .../lib/services/waveform-cache/service.ts | 2 +- .../timeline/components/timeline-element.tsx | 9 +-- .../src/lib/timeline/placement/main-track.ts | 2 +- .../video-editor/src/lib/utils/color.ts | 6 +- .../viewer-3d/src/lib/viewer-3d.tsx | 11 +-- .../src/lib/utils/markdown.ts | 10 +-- .../src/lib/classes/videoOptions.ts | 2 +- 34 files changed, 97 insertions(+), 129 deletions(-) diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx index 3f0e4f3b7a..973ac36977 100644 --- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx @@ -944,7 +944,7 @@ export const ImageTo3DExperience = ({
@@ -1111,7 +1111,7 @@ export const ImageTo3DExperience = ({
{option.label}
- {option.description && option.description} + {option.description}
{showSelected && ( <> diff --git a/frontend/libs/components/gallery-modal/src/lib/FolderNameDialog.tsx b/frontend/libs/components/gallery-modal/src/lib/FolderNameDialog.tsx index 8e232b85ae..26b006c461 100644 --- a/frontend/libs/components/gallery-modal/src/lib/FolderNameDialog.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/FolderNameDialog.tsx @@ -64,7 +64,6 @@ export const FolderNameDialog: React.FC = ({ if (e.key === "Enter") submit(); }} className="w-full rounded-[3px] border border-ui-panel-border bg-ui-controls/40 px-3 py-1.5 text-sm text-base-fg outline-none focus:ring-1 focus:ring-primary/50" - autoFocus />
@@ -47,21 +45,6 @@ export function EditorHeader({ endSlot, exitTo = "/projects" }: EditorHeaderProp ); } -function CurrentUserBadge() { - const { authUser } = useEditorAdapters(); - const user = useAuthUser(authUser); - if (!user) return null; - return ( - // - // {user.displayName} - // - null - ); -} - function ProjectDropdown({ exitTo }: { exitTo: string }) { const [openDialog, setOpenDialog] = useState< "delete" | "rename" | "shortcuts" | null diff --git a/frontend/libs/components/video-editor/src/lib/components/ui/alert.tsx b/frontend/libs/components/video-editor/src/lib/components/ui/alert.tsx index cfcf4cbc84..84c4a648d4 100644 --- a/frontend/libs/components/video-editor/src/lib/components/ui/alert.tsx +++ b/frontend/libs/components/video-editor/src/lib/components/ui/alert.tsx @@ -44,7 +44,7 @@ Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes ->(({ className, ...props }, ref) => ( +>(({ className, children, ...props }, ref) => (
+ > + {children} +
)); AlertTitle.displayName = "AlertTitle"; diff --git a/frontend/libs/components/video-editor/src/lib/core/managers/audio-manager.ts b/frontend/libs/components/video-editor/src/lib/core/managers/audio-manager.ts index 8f0aaea5a0..85a7e00054 100644 --- a/frontend/libs/components/video-editor/src/lib/core/managers/audio-manager.ts +++ b/frontend/libs/components/video-editor/src/lib/core/managers/audio-manager.ts @@ -539,7 +539,7 @@ export class AudioManager { }): Promise { const cacheKey = this.buildPreparedClipCacheKey({ clip }); const existing = this.preparedClipBuffers.get(cacheKey); - if (existing) { + if (existing !== undefined) { return existing; } @@ -574,7 +574,7 @@ export class AudioManager { clip: AudioClipSource; }): Promise { const existing = this.decodedBuffers.get(clip.sourceKey); - if (existing) { + if (existing !== undefined) { return existing; } diff --git a/frontend/libs/components/video-editor/src/lib/fonts/google-fonts.ts b/frontend/libs/components/video-editor/src/lib/fonts/google-fonts.ts index d3303c6b06..fa537feb4c 100644 --- a/frontend/libs/components/video-editor/src/lib/fonts/google-fonts.ts +++ b/frontend/libs/components/video-editor/src/lib/fonts/google-fonts.ts @@ -26,7 +26,7 @@ export function clearFontAtlasCache(): void { export function loadFontAtlas(): Promise { if (cachedAtlas) return Promise.resolve(cachedAtlas); - if (atlasFetchPromise) return atlasFetchPromise; + if (atlasFetchPromise !== null) return atlasFetchPromise; atlasFetchPromise = fetch(FONT_ATLAS_PATH) .then(async (response) => { diff --git a/frontend/libs/components/video-editor/src/lib/fonts/use-font-atlas.ts b/frontend/libs/components/video-editor/src/lib/fonts/use-font-atlas.ts index cb71f70c3d..a4d7fc12ae 100644 --- a/frontend/libs/components/video-editor/src/lib/fonts/use-font-atlas.ts +++ b/frontend/libs/components/video-editor/src/lib/fonts/use-font-atlas.ts @@ -46,7 +46,9 @@ export function useFontAtlas({ open }: { open: boolean }) { const fontNames = useMemo(() => { if (!atlas) return []; - return [...Object.keys(atlas.fonts), ...SYSTEM_FONTS].sort(); + return [...Object.keys(atlas.fonts), ...SYSTEM_FONTS].sort((a, b) => + a.localeCompare(b), + ); }, [atlas]); return { atlas, status, fontNames, retry }; diff --git a/frontend/libs/components/video-editor/src/lib/guides/definitions/grid.tsx b/frontend/libs/components/video-editor/src/lib/guides/definitions/grid.tsx index afa74a773a..e957301e35 100644 --- a/frontend/libs/components/video-editor/src/lib/guides/definitions/grid.tsx +++ b/frontend/libs/components/video-editor/src/lib/guides/definitions/grid.tsx @@ -7,7 +7,6 @@ import { HugeiconsIcon } from "@hugeicons/react"; import { GRID_MIN, GRID_MAX, - DEFAULT_GRID_CONFIG, } from "../grid"; import { usePreviewStore } from "../../preview/preview-store"; import { clampRound } from "../../utils/math"; @@ -113,9 +112,6 @@ function GridGuideOptions() { }} /> - {/* Acknowledge DEFAULT_GRID_CONFIG so the reset import stays parked - for when NumberField (with its onReset affordance) is ported. */} - {DEFAULT_GRID_CONFIG ? null : null}
); } diff --git a/frontend/libs/components/video-editor/src/lib/preview/preview-snap.ts b/frontend/libs/components/video-editor/src/lib/preview/preview-snap.ts index 48bc5f2767..11ed819659 100644 --- a/frontend/libs/components/video-editor/src/lib/preview/preview-snap.ts +++ b/frontend/libs/components/video-editor/src/lib/preview/preview-snap.ts @@ -75,7 +75,7 @@ function pickClosestScaleCandidate - current.distance < closest.distance ? current : closest, + return snapCandidatesWithinThreshold.reduce( + (closest, current) => + current.distance < closest.distance ? current : closest, + snapCandidatesWithinThreshold[0], ); } diff --git a/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/image-node.ts b/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/image-node.ts index c64e9809e1..67f6d18e3f 100644 --- a/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/image-node.ts +++ b/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/image-node.ts @@ -35,7 +35,7 @@ export function loadImageSource({ const cacheKey = `${url}::${maxSourceSize ?? "full"}`; const cached = imageSourceCache.get(cacheKey); - if (cached) return cached; + if (cached !== undefined) return cached; const promise = (async (): Promise => { const image = new Image(); diff --git a/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/sticker-node.ts b/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/sticker-node.ts index 6a15d04c79..ceb23a9108 100644 --- a/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/sticker-node.ts +++ b/frontend/libs/components/video-editor/src/lib/services/renderer/nodes/sticker-node.ts @@ -25,7 +25,7 @@ export function loadStickerSource({ stickerId: string; }): Promise { const cached = stickerSourceCache.get(stickerId); - if (cached) return cached; + if (cached !== undefined) return cached; const promise = (async (): Promise => { const url = resolveStickerId({ diff --git a/frontend/libs/components/video-editor/src/lib/services/video-cache/service.ts b/frontend/libs/components/video-editor/src/lib/services/video-cache/service.ts index 01f230698e..e32326c32f 100644 --- a/frontend/libs/components/video-editor/src/lib/services/video-cache/service.ts +++ b/frontend/libs/components/video-editor/src/lib/services/video-cache/service.ts @@ -170,7 +170,7 @@ export class VideoCache { try { while (true) { - if (sinkData.prefetching && sinkData.prefetchPromise) { + if (sinkData.prefetching && sinkData.prefetchPromise !== null) { await sinkData.prefetchPromise; } @@ -215,7 +215,7 @@ export class VideoCache { time: number; }): Promise { try { - if (sinkData.prefetching && sinkData.prefetchPromise) { + if (sinkData.prefetching && sinkData.prefetchPromise !== null) { await sinkData.prefetchPromise; } 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 index 59fbb914e8..48da5f0d89 100644 --- 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 @@ -27,7 +27,7 @@ export class WaveformCache { audioUrl, }: GetSourceWaveformSummaryArgs): Promise { const existing = this.summaries.get(sourceKey); - if (existing) { + if (existing !== undefined) { return existing; } diff --git a/frontend/libs/components/video-editor/src/lib/timeline/components/timeline-element.tsx b/frontend/libs/components/video-editor/src/lib/timeline/components/timeline-element.tsx index 6a1342b583..9126806e74 100644 --- a/frontend/libs/components/video-editor/src/lib/timeline/components/timeline-element.tsx +++ b/frontend/libs/components/video-editor/src/lib/timeline/components/timeline-element.tsx @@ -47,7 +47,6 @@ import { mediaSupportsAudio } from "../../media/media-utils"; import { canToggleSourceAudio, getSourceAudioActionLabel, - isSourceAudioSeparated, } from "../audio-separation"; import { buildWaveformGainSamples, isElementMuted } from "../audio-state"; import { getTimelinePixelsPerSecond } from "../pixel-utils"; @@ -346,8 +345,6 @@ export function TimelineElement({ element.type === "video" ? getSourceAudioActionLabel({ element }) : "Extract audio"; - const isElementSourceAudioSeparated = - element.type === "video" && isSourceAudioSeparated({ element }); const hasKeyframes = elementKeyframes.length > 0; const expansionHeight = getExpansionHeight({ rows: expandedRows }); const baseTrackHeight = getTrackHeight({ type: track.type }); @@ -450,11 +447,7 @@ export function TimelineElement({ {canToggleCurrentSourceAudio && ( + } onClick={(event: React.MouseEvent) => { event.stopPropagation(); diff --git a/frontend/libs/components/video-editor/src/lib/timeline/placement/main-track.ts b/frontend/libs/components/video-editor/src/lib/timeline/placement/main-track.ts index a3c6152614..5a46d56534 100644 --- a/frontend/libs/components/video-editor/src/lib/timeline/placement/main-track.ts +++ b/frontend/libs/components/video-editor/src/lib/timeline/placement/main-track.ts @@ -26,7 +26,7 @@ export function getEarliestMainTrackElement({ return element.startTime < earliestElement.startTime ? element : earliestElement; - }); + }, elements[0]); } export function enforceMainTrackStart({ diff --git a/frontend/libs/components/video-editor/src/lib/utils/color.ts b/frontend/libs/components/video-editor/src/lib/utils/color.ts index a792894150..20839ed74a 100644 --- a/frontend/libs/components/video-editor/src/lib/utils/color.ts +++ b/frontend/libs/components/video-editor/src/lib/utils/color.ts @@ -54,10 +54,8 @@ export function appendAlpha({ function stripCssNoise({ text }: { text: string }): string { let cleaned = text.trim(); - cleaned = cleaned - .replace(/\s*!important\s*/gi, "") - .replace(/;+\s*$/, "") - .trim(); + cleaned = cleaned.replace(/!important/gi, " ").trim(); + while (cleaned.endsWith(";")) cleaned = cleaned.slice(0, -1).trimEnd(); const colonIndex = cleaned.indexOf(":"); const parenIndex = cleaned.indexOf("("); diff --git a/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx index 0ddde8f2ce..a3808f86b8 100644 --- a/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx +++ b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx @@ -408,9 +408,8 @@ export function Viewer3D({ if (SPLAT_EXTENSIONS.includes(urlExtension)) { console.log(`[Viewer3D] splat format detected (.${urlExtension})`); - new SplatMesh({ - url: modelUrl, - onLoad: (mesh) => { + void new SplatMesh({ url: modelUrl }).initialized + .then((mesh) => { if (cancelled) { mesh.dispose(); return; @@ -434,8 +433,10 @@ export function Viewer3D({ } console.log("Splat loaded"); - }, - }); + }) + .catch((error) => + console.error("[Viewer3D] splat load failed:", error), + ); } else if (!GLTF_EXTENSIONS.includes(urlExtension)) { // Known non-glTF model format with no loader wired here (obj, fbx, // pmx, …). Show the fallback shape rather than spamming a parse error. diff --git a/frontend/libs/markdown-content/src/lib/utils/markdown.ts b/frontend/libs/markdown-content/src/lib/utils/markdown.ts index a50469bc47..c240865e5d 100644 --- a/frontend/libs/markdown-content/src/lib/utils/markdown.ts +++ b/frontend/libs/markdown-content/src/lib/utils/markdown.ts @@ -143,7 +143,7 @@ export const markdownToHtml = ( return renderImage(safeUrl, alt, size); }); // links: [text](url) - out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => { + out = out.replace(/\[([^\][]+)\]\(([^)]+)\)/g, (_m, label, url) => { const safeLabel = String(label || "").replace(/"/g, """); const safeUrl = normalizeLink(String(url || "")); return `${safeLabel}`; @@ -220,7 +220,7 @@ export const markdownToHtml = ( // Image/GIF embed: @image(url, size) or @gif(url, size) const imageMatch = ltrim.match( - /^@(image|gif)\(([^,)]+)(?:,\s*([^)]+))?\)\s*$/, + /^@(image|gif)\(([^,)]+)(?:,([^)]+))?\)\s*$/, ); if (imageMatch) { flushParagraph(); @@ -232,12 +232,12 @@ export const markdownToHtml = ( } // ATX headings: support #..###### with optional space after hashes - const heading = ltrim.match(/^(#{1,6})\s*(.*)$/); + const heading = ltrim.match(/^(#{1,6})(.*)$/); if (heading) { flushParagraph(); closeListIfOpen(); const level = Math.min(heading[1].length, 6); - const text = renderInline(heading[2] || ""); + const text = renderInline((heading[2] || "").trimStart()); htmlParts.push(`${text}`); continue; } @@ -318,7 +318,7 @@ export const slugify = (s: string): string => .replace(/[_\s]+/g, "-") .replace(/[^a-z0-9-]/g, "") .replace(/--+/g, "-") - .replace(/^-+|-+$/g, ""); + .replace(/^-|-$/g, ""); export const pathToFilename = (path: string): string => { const parts = path.split("/"); diff --git a/frontend/libs/model-list/src/lib/classes/videoOptions.ts b/frontend/libs/model-list/src/lib/classes/videoOptions.ts index 7657fb5c05..76d07dd229 100644 --- a/frontend/libs/model-list/src/lib/classes/videoOptions.ts +++ b/frontend/libs/model-list/src/lib/classes/videoOptions.ts @@ -70,7 +70,7 @@ export function resolveVideoDuration( (distance === bestDistance && option > best) ? option : best; - }); + }, model.durationOptions[0]); } return model.defaultDuration ?? null; }