From e8a74eb79d79023cf9cbce9ea83c73d628107276 Mon Sep 17 00:00:00 2001 From: Mary Hipp Rogers Date: Thu, 28 Aug 2025 08:26:09 -0400 Subject: [PATCH] Revert "feat(ui): gallery optimistic updates for video" This reverts commit 0ec6d33086123be11efd890939ff29d07a23e6d1. --- .../web/src/services/api/endpoints/videos.ts | 39 +--- .../services/api/util/optimisticUpdates.ts | 59 +----- .../services/events/onInvocationComplete.tsx | 171 ++---------------- 3 files changed, 22 insertions(+), 247 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/endpoints/videos.ts b/invokeai/frontend/web/src/services/api/endpoints/videos.ts index 88036168a9..6366e70c37 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/videos.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/videos.ts @@ -1,14 +1,14 @@ -import { getStore } from 'app/store/nanostores/store'; import type { paths } from 'services/api/schema'; -import type { GetVideoIdsArgs, GetVideoIdsResult, VideoDTO } from 'services/api/types'; -import { - getTagsToInvalidateForBoardAffectingMutation, - getTagsToInvalidateForVideoMutation, -} from 'services/api/util/tagInvalidation'; +import type { + GetVideoIdsArgs, + GetVideoIdsResult, + VideoDTO, +} from 'services/api/types'; import stableHash from 'stable-hash'; import type { Param0 } from 'tsafe'; import { api, buildV1Url, LIST_TAG } from '..'; +import { getTagsToInvalidateForBoardAffectingMutation, getTagsToInvalidateForImageMutation, getTagsToInvalidateForVideoMutation } from '../util/tagInvalidation'; /** * Builds an endpoint URL for the videos router @@ -16,10 +16,10 @@ import { api, buildV1Url, LIST_TAG } from '..'; * buildVideosUrl('some-path') * // '/api/v1/videos/some-path' */ -const buildVideosUrl = (path: string = '', query?: Parameters[1]) => +const buildVideosUrl = (path: string = '', query?: Parameters[1]) => buildV1Url(`videos/${path}`, query); -const buildBoardVideosUrl = (path: string = '') => buildV1Url(`board_videos/${path}`); +const buildBoardVideosUrl = (path: string = '') => buildV1Url(`board_videos/${path}`); export const videosApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -31,6 +31,7 @@ export const videosApi = api.injectEndpoints({ query: (video_id) => ({ url: buildVideosUrl(`i/${video_id}`) }), providesTags: (result, error, video_id) => [{ type: 'Video', id: video_id }], }), + /** * Get ordered list of image names for selection operations @@ -203,24 +204,4 @@ export const { useRemoveVideosFromBoardMutation, } = videosApi; -/** - * Imperative RTKQ helper to fetch an VideoDTO. - * @param id The id of the video to fetch - * @param options The options for the query. By default, the query will not subscribe to the store. - * @returns The ImageDTO if found, otherwise null - */ -export const getVideoDTOSafe = async ( - id: string, - options?: Parameters[1] -): Promise => { - const _options = { - subscribe: false, - ...options, - }; - const req = getStore().dispatch(videosApi.endpoints.getVideoDTOsByNames.initiate({ video_ids: [id] }, _options)); - try { - return (await req.unwrap())[0] ?? null; - } catch { - return null; - } -}; + diff --git a/invokeai/frontend/web/src/services/api/util/optimisticUpdates.ts b/invokeai/frontend/web/src/services/api/util/optimisticUpdates.ts index ca79a99ca4..772dc077fb 100644 --- a/invokeai/frontend/web/src/services/api/util/optimisticUpdates.ts +++ b/invokeai/frontend/web/src/services/api/util/optimisticUpdates.ts @@ -1,5 +1,5 @@ import type { OrderDir } from 'features/gallery/store/types'; -import type { GetImageNamesResult, GetVideoIdsResult, ImageDTO, VideoDTO } from 'services/api/types'; +import type { GetImageNamesResult, ImageDTO } from 'services/api/types'; /** * Calculates the optimal insertion position for a new image in the names list. @@ -57,60 +57,3 @@ export function insertImageIntoNamesResult( total_count: currentResult.total_count + 1, }; } - -/** - * Calculates the optimal insertion position for a new image in the names list. - * For starred_first=true: starred images go to position 0, unstarred go after all starred images - * For starred_first=false: all new images go to position 0 (newest first) - */ -function calculateVideoInsertionPosition( - videoDTO: VideoDTO, - starredFirst: boolean, - starredCount: number, - orderDir: OrderDir = 'DESC' -): number { - if (!starredFirst) { - // When starred_first is false, insertion depends on order direction - return orderDir === 'DESC' ? 0 : Number.MAX_SAFE_INTEGER; - } - - // When starred_first is true - if (videoDTO.starred) { - // Starred images: beginning for desc, after existing starred for asc - return orderDir === 'DESC' ? 0 : starredCount; - } - - // Unstarred images go after all starred images - return orderDir === 'DESC' ? starredCount : Number.MAX_SAFE_INTEGER; -} - -/** - * Optimistically inserts a new image into the ImageNamesResult at the correct position - */ -export function insertVideoIntoGetVideoIdsResult( - currentResult: GetVideoIdsResult, - videoDTO: VideoDTO, - starredFirst: boolean, - orderDir: OrderDir = 'DESC' -): GetVideoIdsResult { - // Don't insert if the image is already in the list - if (currentResult.video_ids.includes(videoDTO.video_id)) { - return currentResult; - } - - const insertPosition = calculateVideoInsertionPosition(videoDTO, starredFirst, currentResult.starred_count, orderDir); - - const newVideoIds = [...currentResult.video_ids]; - // Handle MAX_SAFE_INTEGER by pushing to end - if (insertPosition >= newVideoIds.length) { - newVideoIds.push(videoDTO.video_id); - } else { - newVideoIds.splice(insertPosition, 0, videoDTO.video_id); - } - - return { - video_ids: newVideoIds, - starred_count: starredFirst && videoDTO.starred ? currentResult.starred_count + 1 : currentResult.starred_count, - total_count: currentResult.total_count + 1, - }; -} diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx index 8266485212..bdf02cc26d 100644 --- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx +++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx @@ -5,7 +5,6 @@ import { selectAutoSwitch, selectGalleryView, selectGetImageNamesQueryArgs, - selectGetVideoIdsQueryArgs, selectListBoardsQueryArgs, selectSelectedBoardId, } from 'features/gallery/store/gallerySelectors'; @@ -18,10 +17,9 @@ import { generatedVideoChanged } from 'features/parameters/store/videoSlice'; import type { LRUCache } from 'lru-cache'; import { boardsApi } from 'services/api/endpoints/boards'; import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images'; -import { getVideoDTOSafe, videosApi } from 'services/api/endpoints/videos'; -import type { ImageDTO, S, VideoDTO } from 'services/api/types'; +import type { ImageDTO, S } from 'services/api/types'; import { getCategories } from 'services/api/util'; -import { insertImageIntoNamesResult, insertVideoIntoGetVideoIdsResult } from 'services/api/util/optimisticUpdates'; +import { insertImageIntoNamesResult } from 'services/api/util/optimisticUpdates'; import { $lastProgressEvent } from 'services/events/stores'; import stableHash from 'stable-hash'; import type { Param0 } from 'tsafe'; @@ -187,154 +185,6 @@ export const buildOnInvocationComplete = ( } }; - const addVideosToGallery = async (data: S['InvocationCompleteEvent']) => { - if (nodeTypeDenylist.includes(data.invocation.type)) { - log.trace(`Skipping denylisted node type (${data.invocation.type})`); - return; - } - - const videoDTOs = await getResultVideoDTOs(data); - if (videoDTOs.length === 0) { - return; - } - - // For efficiency's sake, we want to minimize the number of dispatches and invalidations we do. - // We'll keep track of each change we need to make and do them all at once. - const boardTotalAdditions: Record = {}; - const getVideoIdsArg = selectGetVideoIdsQueryArgs(getState()); - - for (const videoDTO of videoDTOs) { - if (videoDTO.is_intermediate) { - return; - } - - const board_id = videoDTO.board_id ?? 'none'; - - boardTotalAdditions[board_id] = (boardTotalAdditions[board_id] || 0) + 1; - } - - // Update all the board image totals at once - const entries: Param0 = []; - for (const [boardId, amountToAdd] of objectEntries(boardTotalAdditions)) { - // upsertQueryEntries doesn't provide a "recipe" function for the update - we must provide the new value - // directly. So we need to select the board totals first. - const total = boardsApi.endpoints.getBoardImagesTotal.select(boardId)(getState()).data?.total; - if (total === undefined) { - // No cache exists for this board, so we can't update it. - continue; - } - entries.push({ - endpointName: 'getBoardImagesTotal', - arg: boardId, - value: { total: total + amountToAdd }, - }); - } - dispatch(boardsApi.util.upsertQueryEntries(entries)); - - dispatch( - boardsApi.util.updateQueryData('listAllBoards', selectListBoardsQueryArgs(getState()), (draft) => { - for (const board of draft) { - board.image_count = board.image_count + (boardTotalAdditions[board.board_id] ?? 0); - } - }) - ); - - /** - * Optimistic update and cache invalidation for image names queries that match this image's board and categories. - * - Optimistic update for the cache that does not have a search term (we cannot derive the correct insertion - * position when a search term is present). - * - Cache invalidation for the query that has a search term, so it will be refetched. - * - * Note: The image DTO objects are already implicitly cached by the getResultImageDTOs function. We do not need - * to explicitly cache them again here. - */ - for (const videoDTO of videoDTOs) { - // Override board_id and categories for this specific image to build the "expected" args for the query. - const videoSpecificArgs = { - board_id: videoDTO.board_id ?? 'none', - }; - - const expectedQueryArgs = { - ...getVideoIdsArg, - ...videoSpecificArgs, - search_term: '', - }; - - // If the cache for the query args provided here does not exist, RTK Query will ignore the update. - dispatch( - videosApi.util.updateQueryData( - 'getVideoIds', - { - ...getVideoIdsArg, - ...videoSpecificArgs, - search_term: '', - }, - (draft) => { - const updatedResult = insertVideoIntoGetVideoIdsResult( - draft, - videoDTO, - expectedQueryArgs.starred_first ?? true, - expectedQueryArgs.order_dir - ); - - draft.video_ids = updatedResult.video_ids; - draft.starred_count = updatedResult.starred_count; - draft.total_count = updatedResult.total_count; - } - ) - ); - - // If there is a search term present, we need to invalidate that query to ensure the search results are updated. - if (getVideoIdsArg.search_term) { - const expectedQueryArgs = { - ...getVideoIdsArg, - ...videoSpecificArgs, - }; - dispatch(imagesApi.util.invalidateTags([{ type: 'ImageNameList', id: stableHash(expectedQueryArgs) }])); - } - } - - // No need to invalidate tags since we're doing optimistic updates - // Board totals are already updated above via upsertQueryEntries - - const autoSwitch = selectAutoSwitch(getState()); - - if (!autoSwitch) { - return; - } - - // Finally, we may need to autoswitch to the new image. We'll only do it for the last image in the list. - const lastVideoDTO = videoDTOs.at(-1); - - if (!lastVideoDTO) { - return; - } - - const { video_id } = lastVideoDTO; - const board_id = lastVideoDTO.board_id ?? 'none'; - - // With optimistic updates, we can immediately switch to the new image - const selectedBoardId = selectSelectedBoardId(getState()); - - // If the image is from a different board, switch to that board & select the image - otherwise just select the - // image. This implicitly changes the view to 'images' if it was not already. - if (board_id !== selectedBoardId) { - dispatch( - boardIdSelected({ - boardId: board_id, - selectedImageName: video_id, - }) - ); - } else { - // Ensure we are on the 'images' gallery view - that's where this image will be displayed - const galleryView = selectGalleryView(getState()); - if (galleryView !== 'videos') { - dispatch(galleryViewChanged('videos')); - } - // Select the image immediately since we've optimistically updated the cache - dispatch(imageSelected(lastVideoDTO.video_id)); - } - }; const getResultImageDTOs = async (data: S['InvocationCompleteEvent']): Promise => { const { result } = data; const imageDTOs: ImageDTO[] = []; @@ -356,20 +206,17 @@ export const buildOnInvocationComplete = ( return imageDTOs; }; - const getResultVideoDTOs = async (data: S['InvocationCompleteEvent']): Promise => { + const getResultVideoFields = (data: S['InvocationCompleteEvent']): VideoField[] => { const { result } = data; - const videoDTOs: VideoDTO[] = []; + const videoFields: VideoField[] = []; for (const [_name, value] of objectEntries(result)) { if (isVideoField(value)) { - const videoDTO = await getVideoDTOSafe(value.video_id); - if (videoDTO) { - videoDTOs.push(videoDTO); - } + videoFields.push(value); } } - return videoDTOs; + return videoFields; }; return async (data: S['InvocationCompleteEvent']) => { @@ -392,7 +239,11 @@ export const buildOnInvocationComplete = ( } await addImagesToGallery(data); - await addVideosToGallery(data); + + const videoField = getResultVideoFields(data)[0]; + if (videoField) { + dispatch(generatedVideoChanged({ videoField })); + } $lastProgressEvent.set(null); };