From 71adfd4701f7164bb7b527d27cecf24955cba02b Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:35:11 +0100 Subject: [PATCH] artcraft app: mark fire-and-forget promises with void (Sonar S9383) (#2806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves all 38 SonarCloud **typescript:S9383** findings ("Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator") in `frontend/apps/artcraft/app`, across 19 files. They are: - startup calls in `index.tsx`, such as `SyncStorytellerApiConfig()` and `persistLogin()` - polling in `useActiveJobs`, `useBackgroundLoadingMedia`, `signals/activeJobs.ts` and `signals/authentication/fetchers.ts` - listener cleanups (`unlisten*.then(f => f())`) in `TaskQueue` and `useDesktopGenerationFeed` - UI handlers that start uploads or renames, such as `ImageTo3DExperience`, `UploadFilesMedia`, `SceneTitleInput`, `ProfileDropdown` and `DesktopGenerationGallery` - async IIFEs in `useDesktopUsername`, `TaskQueue` and `BaseImageSelector` This is part 2 of 3 for artcraft's 157 S9383 findings. Part 1 (Tauri libraries) is #2805. Part 3 (`libs/components`) is #2807. Several of these files are also changed by open PRs for unrelated reasons, for example #2519, #2520, #2615 and #2064. Each change here inserts one word at the start of a statement, so a rebase either way is trivial. ## Approach This ports the approach of storytold/artcraft-services#179, which fixes the same rule in artcraft-services. **#179 is still open and unmerged.** This PR follows its approach and does not depend on it. - Each flagged expression is a fire-and-forget promise: a listener `setup()`, an `unlisten.then(f => f())` cleanup, a loader called from an effect, an upload kicked off from a handler, or an async IIFE. Prefixing it with `void` marks the ignored promise explicitly, which is the fix the rule documents. - It deliberately does not add `.catch()` everywhere. These call sites already handle failures inside the async function or surface them through their own state, and empty handlers would hide errors. - Async IIFEs become `void (async () => …)()`. `if (cond) call()` becomes `if (cond) void call()`. - There is no behaviour change. `void` only discards the promise value, which was already being discarded. Each insertion is placed at the exact `textRange` start offset of the SonarCloud finding, with no other edits. ## Testing - Each changed file parses with `esbuild `, using the same check as #179. - I parsed each changed file with the TypeScript 5.9 compiler API (`createSourceFile`). That found no parse errors, and every inserted `void` is either a whole expression statement or an arrow-function body applied to a call, never part of a larger expression. - `frontend/.eslintrc.json` has no `no-void` rule. - I did not run the full Nx type-check, lint or test targets, because `frontend/node_modules` is not installed in my environment. Please rely on CI. - No tests are added. Discarding an already-discarded promise value is not observable, so no test can fail before this change and pass after it. ## Summary by CodeRabbit * **Chores** * Updated background tasks across uploads, sign-in, polling, scene editing, and other app workflows to handle asynchronous operations consistently. * Existing workflows and their success and error handling remain unchanged; no user-facing behavior changes are expected. --- .../GlobalFileDropHandler.tsx | 2 +- .../components/experiences/ImageTo3DExperience.tsx | 8 ++++---- .../generation-feed/DesktopGenerationGallery.tsx | 2 +- .../generation-feed/useDesktopGenerationFeed.ts | 4 ++-- .../generation-feed/useDesktopUsername.ts | 2 +- .../reusable/UploadModalMedia/UploadFilesMedia.tsx | 2 +- .../components/signaled/TopBar/ProfileDropdown.tsx | 2 +- .../components/signaled/TopBar/SceneTitleInput.tsx | 2 +- .../src/components/signaled/TopBar/TaskQueue.tsx | 14 +++++++------- .../components/signaled/TopBar/taskEnqueueMeta.ts | 2 +- .../artcraft/app/src/hoc/withProtectedRoute.tsx | 2 +- .../app/src/hooks/useActiveJobs/useActiveJobs.ts | 6 +++--- .../useBackgroundLoadingMedia.tsx | 8 ++++---- frontend/apps/artcraft/app/src/index.tsx | 4 ++-- frontend/apps/artcraft/app/src/pages/MainApp.tsx | 2 +- .../app/src/pages/PageDraw/BaseImageSelector.tsx | 2 +- .../PageRemoveBackground/RemoveBackground.tsx | 2 +- .../apps/artcraft/app/src/signals/activeJobs.ts | 2 +- .../app/src/signals/authentication/fetchers.ts | 8 ++++---- 19 files changed, 38 insertions(+), 38 deletions(-) diff --git a/frontend/apps/artcraft/app/src/components/GlobalFileDropHandler/GlobalFileDropHandler.tsx b/frontend/apps/artcraft/app/src/components/GlobalFileDropHandler/GlobalFileDropHandler.tsx index 368189837e..6eabbacd30 100644 --- a/frontend/apps/artcraft/app/src/components/GlobalFileDropHandler/GlobalFileDropHandler.tsx +++ b/frontend/apps/artcraft/app/src/components/GlobalFileDropHandler/GlobalFileDropHandler.tsx @@ -120,7 +120,7 @@ export function GlobalFileDropHandler() { } }; - setup(); + void setup(); return () => { unlisteners.forEach((fn) => fn()); }; } else { diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx index 24d2fdc59d..3f0e4f3b7a 100644 --- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx @@ -256,7 +256,7 @@ export const ImageTo3DExperience = ({ }; const handleFileChange = (event: React.ChangeEvent) => { - handleFiles(event.target.files); + void handleFiles(event.target.files); event.target.value = ""; }; @@ -264,7 +264,7 @@ export const ImageTo3DExperience = ({ event.preventDefault(); event.stopPropagation(); setDragActive(false); - handleFiles(event.dataTransfer?.files); + void handleFiles(event.dataTransfer?.files); }; const handlePickFromLibrary = () => { @@ -716,7 +716,7 @@ export const ImageTo3DExperience = ({ e.stopPropagation(); setDragActive(false); if (e.dataTransfer?.files) - handleWorldFiles( + void handleWorldFiles( Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/"), ), @@ -1177,7 +1177,7 @@ export const ImageTo3DExperience = ({ className="hidden" onChange={(e) => { if (e.target.files) { - handleWorldFiles(Array.from(e.target.files)); + void handleWorldFiles(Array.from(e.target.files)); } e.target.value = ""; }} diff --git a/frontend/apps/artcraft/app/src/components/generation-feed/DesktopGenerationGallery.tsx b/frontend/apps/artcraft/app/src/components/generation-feed/DesktopGenerationGallery.tsx index 327031844a..10cd58bc27 100644 --- a/frontend/apps/artcraft/app/src/components/generation-feed/DesktopGenerationGallery.tsx +++ b/frontend/apps/artcraft/app/src/components/generation-feed/DesktopGenerationGallery.tsx @@ -248,7 +248,7 @@ function ItemActions({ e.stopPropagation(); const url = item.fullImage || item.thumbnail; if (!url) return; - applyMakeVideoFromImage(url, item.id); + void applyMakeVideoFromImage(url, item.id); }, [item.fullImage, item.thumbnail, item.id], ); diff --git a/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopGenerationFeed.ts b/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopGenerationFeed.ts index e0923cbf03..0014c75238 100644 --- a/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopGenerationFeed.ts +++ b/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopGenerationFeed.ts @@ -295,8 +295,8 @@ export function useDesktopGenerationFeed(options: { cancelled = true; clearInterval(intervalId); window.removeEventListener("task-queue-update", handleTaskUpdate); - unlistenComplete.then((f) => f()); - unlistenFailed.then((f) => f()); + void unlistenComplete.then((f) => f()); + void unlistenFailed.then((f) => f()); }; }, [load]); diff --git a/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopUsername.ts b/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopUsername.ts index 444fe7d031..d3658dc4b6 100644 --- a/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopUsername.ts +++ b/frontend/apps/artcraft/app/src/components/generation-feed/useDesktopUsername.ts @@ -8,7 +8,7 @@ export function useDesktopUsername(): string | null { useEffect(() => { let cancelled = false; - (async () => { + void (async () => { try { const session = await new UsersApi().GetSession(); if (!cancelled && session.success && session.data?.user) { diff --git a/frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadFilesMedia.tsx b/frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadFilesMedia.tsx index 4fe4eaec30..8856eceec2 100644 --- a/frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadFilesMedia.tsx +++ b/frontend/apps/artcraft/app/src/components/reusable/UploadModalMedia/UploadFilesMedia.tsx @@ -48,7 +48,7 @@ export const UploadFilesMedia = ({ return; } - uploadPlane({ + void uploadPlane({ title: uploadTitle.value, assetFile: assetFile.value, progressCallback: onUploadProgress, diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/ProfileDropdown.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/ProfileDropdown.tsx index 459a5b086b..be6f1b5faa 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/ProfileDropdown.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/ProfileDropdown.tsx @@ -26,7 +26,7 @@ export default function ProfileDropdown() { label: "Logout", icon: LogOutIcon, onClick: () => { - logout(); + void logout(); }, }, ]; diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/SceneTitleInput.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/SceneTitleInput.tsx index 1dd589e2b1..2120bd4b0e 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/SceneTitleInput.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/SceneTitleInput.tsx @@ -67,7 +67,7 @@ export const SceneTitleInput = ({ pageName }: Props) => { e.currentTarget.focus(); } else if (scene.value.token) { setIsSaving(true); - renameScene( + void renameScene( scene.value.title!, //guarunteed by input scene.value.token, ); diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx index b710abc9f3..3d928c87b2 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx @@ -164,7 +164,7 @@ const CopyPromptButton = ({ prompt }: { prompt: string }) => { aria-label="Copy prompt" onClick={(e) => { e.stopPropagation(); - navigator.clipboard.writeText(prompt); + void navigator.clipboard.writeText(prompt); setCopied(true); setTimeout(() => setCopied(false), 3000); }} @@ -869,7 +869,7 @@ export const TaskQueue = () => { }; cleanupOldEntries(); - load(); + void load(); const id = setInterval(load, 5000); // Listen for cover image uploads to refresh and show new thumbnails @@ -883,17 +883,17 @@ export const TaskQueue = () => { let unlistenComplete: Promise | null = null; let unlistenFailed: Promise | null = null; - (async () => { + void (async () => { // Update immediately when Tauri signals a generation completion unlistenComplete = listen("generation-complete-event", () => { if (!cancelled) { - load(); + void load(); } }); // Also update immediately when a generation fails unlistenFailed = listen("generation-failed-event", () => { if (!cancelled) { - load(); + void load(); // Refunded credits may take a moment to settle in the database setTimeout(() => { useCreditsState.getState().fetchFromServer(); @@ -906,10 +906,10 @@ export const TaskQueue = () => { clearInterval(id); window.removeEventListener("cover-image-uploaded", handleCoverUploaded); if (unlistenComplete) { - unlistenComplete.then((f) => f()); + void unlistenComplete.then((f) => f()); } if (unlistenFailed) { - unlistenFailed.then((f) => f()); + void unlistenFailed.then((f) => f()); } }; }, [ diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts b/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts index 567ced7a99..331ad08938 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts @@ -125,7 +125,7 @@ export function storeEnqueueMeta(meta: EnqueueMeta) { // Then convert blob URLs to base64 in the background for persistence if (meta.refImageUrls && meta.refImageUrls.length > 0) { - Promise.all(meta.refImageUrls.map(toBase64Thumb)).then((dataUrls) => { + void Promise.all(meta.refImageUrls.map(toBase64Thumb)).then((dataUrls) => { const current = readEntries(); const found = current.find((e) => e.id === entryId); if (found) { diff --git a/frontend/apps/artcraft/app/src/hoc/withProtectedRoute.tsx b/frontend/apps/artcraft/app/src/hoc/withProtectedRoute.tsx index 5e96ed0bf2..16ea97db39 100644 --- a/frontend/apps/artcraft/app/src/hoc/withProtectedRoute.tsx +++ b/frontend/apps/artcraft/app/src/hoc/withProtectedRoute.tsx @@ -12,7 +12,7 @@ export const withProtectionRoute =

( function ProtectionRoute(rest: P) { useSignals(); useSignalEffect(() => { - persistLogin(); + void persistLogin(); }); // const { status, userInfo } = authentication; diff --git a/frontend/apps/artcraft/app/src/hooks/useActiveJobs/useActiveJobs.ts b/frontend/apps/artcraft/app/src/hooks/useActiveJobs/useActiveJobs.ts index b667c7179d..e0cc622a91 100644 --- a/frontend/apps/artcraft/app/src/hooks/useActiveJobs/useActiveJobs.ts +++ b/frontend/apps/artcraft/app/src/hooks/useActiveJobs/useActiveJobs.ts @@ -38,7 +38,7 @@ export const useActiveJobs = () => { // then set new one with new interval clearInterval(curr.timer); const newTimer = setInterval(() => { - PollRecentJobs(); + void PollRecentJobs(); }, newTimeInterval); return { timer: newTimer, @@ -72,7 +72,7 @@ export const useActiveJobs = () => { useSignalEffect(() => { // if active jobs is not initiated, do do a first pull; if (!activeJobs.value) { - PollRecentJobs(); + void PollRecentJobs(); return; } // if there are no active jobs, stop making polls @@ -115,7 +115,7 @@ export const useActiveJobs = () => { //else make and set timer const newTimer = setInterval(() => { - PollRecentJobs(); + void PollRecentJobs(); }, curr.interval); return { ...curr, diff --git a/frontend/apps/artcraft/app/src/hooks/useBackgroundLoadingMedia/useBackgroundLoadingMedia.tsx b/frontend/apps/artcraft/app/src/hooks/useBackgroundLoadingMedia/useBackgroundLoadingMedia.tsx index 27099abf18..f06ecc6081 100644 --- a/frontend/apps/artcraft/app/src/hooks/useBackgroundLoadingMedia/useBackgroundLoadingMedia.tsx +++ b/frontend/apps/artcraft/app/src/hooks/useBackgroundLoadingMedia/useBackgroundLoadingMedia.tsx @@ -24,7 +24,7 @@ export const useBackgroundLoadingMedia = () => { //CASE 1: first load // if myMovies undefined, poll for the first time if (!userMovies.value) { - PollUserGeneratedMovies(); + void PollUserGeneratedMovies(); return; } @@ -45,7 +45,7 @@ export const useBackgroundLoadingMedia = () => { //there are videos newly completed, set and poll lastCompletedWorkflow.current = completedWorkflowJobs.value; - PollUserGeneratedMovies().then((ret: boolean) => { + void PollUserGeneratedMovies().then((ret: boolean) => { if (ret) { addToast( ToastTypes.SUCCESS, @@ -60,7 +60,7 @@ export const useBackgroundLoadingMedia = () => { //CASE 1: first load // if audioItems undefined, poll for the first time if (!userAudioItems.value) { - PollUserAudioItems(); + void PollUserAudioItems(); return; } @@ -82,7 +82,7 @@ export const useBackgroundLoadingMedia = () => { //there are audio jobs newly completed, set and poll lastCompletedAudioJobs.current = completedAudioJobs.value; - PollUserAudioItems().then((ret: boolean) => { + void PollUserAudioItems().then((ret: boolean) => { if (ret) { addToast( ToastTypes.SUCCESS, diff --git a/frontend/apps/artcraft/app/src/index.tsx b/frontend/apps/artcraft/app/src/index.tsx index 6c433fccf7..8564d92080 100644 --- a/frontend/apps/artcraft/app/src/index.tsx +++ b/frontend/apps/artcraft/app/src/index.tsx @@ -26,12 +26,12 @@ const ENV = { const GlobalSettingsManager = ({ env }: { env: Record }) => { console.log("GlobalSettingsManager()"); - SyncStorytellerApiConfig(); + void SyncStorytellerApiConfig(); useSignals(); useSignalEffect(() => { - persistLogin(); + void persistLogin(); }); /// Initizations that depends on ENV vars /// diff --git a/frontend/apps/artcraft/app/src/pages/MainApp.tsx b/frontend/apps/artcraft/app/src/pages/MainApp.tsx index b35bebd13a..175fefe5ee 100644 --- a/frontend/apps/artcraft/app/src/pages/MainApp.tsx +++ b/frontend/apps/artcraft/app/src/pages/MainApp.tsx @@ -152,7 +152,7 @@ export const MainApp = ({ sceneToken }: Props) => { // Session probe (runs once per shell mount). useEffect(() => { const usersApi = new UsersApi(); - usersApi.GetSession().then((result) => { + void usersApi.GetSession().then((result) => { console.log( `User Info | Username: ${result.data?.user?.username}, Token: ${result.data?.user?.user_token}`, ); diff --git a/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx b/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx index 39a18ab615..e4ecd13384 100644 --- a/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx @@ -84,7 +84,7 @@ export const BaseImageSelector = ({ progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { const mediaToken = newState.data || ""; - (async () => { + void (async () => { let finalUrl = reader.result as string; let thumbnailUrlTemplate = undefined; try { diff --git a/frontend/apps/artcraft/app/src/pages/PageRemoveBackground/RemoveBackground.tsx b/frontend/apps/artcraft/app/src/pages/PageRemoveBackground/RemoveBackground.tsx index 503b928c82..75616bf9f3 100644 --- a/frontend/apps/artcraft/app/src/pages/PageRemoveBackground/RemoveBackground.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageRemoveBackground/RemoveBackground.tsx @@ -148,7 +148,7 @@ export const RemoveBackground = () => { } }; - processExternalUrl(); + void processExternalUrl(); }, [ pendingExternalUrl, setPendingExternalUrl, diff --git a/frontend/apps/artcraft/app/src/signals/activeJobs.ts b/frontend/apps/artcraft/app/src/signals/activeJobs.ts index 60c5ad6992..c1e5673aad 100644 --- a/frontend/apps/artcraft/app/src/signals/activeJobs.ts +++ b/frontend/apps/artcraft/app/src/signals/activeJobs.ts @@ -87,7 +87,7 @@ export const completedAudioJobs = computed(() => { export const shouldPollActiveJobs = signal(false); export const startPollingActiveJobs = () => { - PollRecentJobs(); //poll once regardless of polling intervals + void PollRecentJobs(); //poll once regardless of polling intervals shouldPollActiveJobs.value = true; }; diff --git a/frontend/apps/artcraft/app/src/signals/authentication/fetchers.ts b/frontend/apps/artcraft/app/src/signals/authentication/fetchers.ts index 68e49b7d48..b6f1e3cb8b 100644 --- a/frontend/apps/artcraft/app/src/signals/authentication/fetchers.ts +++ b/frontend/apps/artcraft/app/src/signals/authentication/fetchers.ts @@ -47,7 +47,7 @@ export const login = async ({ // technically user is login with the system now, HOWEVER, // in storyteller studio, only having a sesison is not enough, // we need session info and active subscription info as well - getUserInfoAndSubcriptions(); + void getUserInfoAndSubcriptions(); window.location.href = "/"; // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this. }; @@ -86,7 +86,7 @@ export const signUp = async ({ // technically user is login with the system now, HOWEVER, // in storyteller studio, only having a sesison is not enough, // we need session info and active subscription info as well - getUserInfoAndSubcriptions(); + void getUserInfoAndSubcriptions(); return ""; }; @@ -95,12 +95,12 @@ export const persistLogin = async () => { if (authentication.status.value !== AUTH_STATUS.INIT) { return; } - getUserInfoAndSubcriptions(); + void getUserInfoAndSubcriptions(); }; // NB: Only for SyncStorytellerApiConfig. export const forceGetUserInfoAndSubcriptions = async () => { - getUserInfoAndSubcriptions(); + void getUserInfoAndSubcriptions(); }; async function getUserInfoAndSubcriptions() {