5
5
selectAutoSwitch ,
6
6
selectGalleryView ,
7
7
selectGetImageNamesQueryArgs ,
8
+ selectGetVideoIdsQueryArgs ,
8
9
selectListBoardsQueryArgs ,
9
10
selectSelectedBoardId ,
10
11
} from 'features/gallery/store/gallerySelectors' ;
@@ -17,9 +18,10 @@ import { generatedVideoChanged } from 'features/parameters/store/videoSlice';
17
18
import type { LRUCache } from 'lru-cache' ;
18
19
import { boardsApi } from 'services/api/endpoints/boards' ;
19
20
import { getImageDTOSafe , imagesApi } from 'services/api/endpoints/images' ;
20
- import type { ImageDTO , S } from 'services/api/types' ;
21
+ import { getVideoDTOSafe , videosApi } from 'services/api/endpoints/videos' ;
22
+ import type { ImageDTO , S , VideoDTO } from 'services/api/types' ;
21
23
import { getCategories } from 'services/api/util' ;
22
- import { insertImageIntoNamesResult } from 'services/api/util/optimisticUpdates' ;
24
+ import { insertImageIntoNamesResult , insertVideoIntoGetVideoIdsResult } from 'services/api/util/optimisticUpdates' ;
23
25
import { $lastProgressEvent } from 'services/events/stores' ;
24
26
import stableHash from 'stable-hash' ;
25
27
import type { Param0 } from 'tsafe' ;
@@ -185,6 +187,154 @@ export const buildOnInvocationComplete = (
185
187
}
186
188
} ;
187
189
190
+ const addVideosToGallery = async ( data : S [ 'InvocationCompleteEvent' ] ) => {
191
+ if ( nodeTypeDenylist . includes ( data . invocation . type ) ) {
192
+ log . trace ( `Skipping denylisted node type (${ data . invocation . type } )` ) ;
193
+ return ;
194
+ }
195
+
196
+ const videoDTOs = await getResultVideoDTOs ( data ) ;
197
+ if ( videoDTOs . length === 0 ) {
198
+ return ;
199
+ }
200
+
201
+ // For efficiency's sake, we want to minimize the number of dispatches and invalidations we do.
202
+ // We'll keep track of each change we need to make and do them all at once.
203
+ const boardTotalAdditions : Record < string , number > = { } ;
204
+ const getVideoIdsArg = selectGetVideoIdsQueryArgs ( getState ( ) ) ;
205
+
206
+ for ( const videoDTO of videoDTOs ) {
207
+ if ( videoDTO . is_intermediate ) {
208
+ return ;
209
+ }
210
+
211
+ const board_id = videoDTO . board_id ?? 'none' ;
212
+
213
+ boardTotalAdditions [ board_id ] = ( boardTotalAdditions [ board_id ] || 0 ) + 1 ;
214
+ }
215
+
216
+ // Update all the board image totals at once
217
+ const entries : Param0 < typeof boardsApi . util . upsertQueryEntries > = [ ] ;
218
+ for ( const [ boardId , amountToAdd ] of objectEntries ( boardTotalAdditions ) ) {
219
+ // upsertQueryEntries doesn't provide a "recipe" function for the update - we must provide the new value
220
+ // directly. So we need to select the board totals first.
221
+ const total = boardsApi . endpoints . getBoardImagesTotal . select ( boardId ) ( getState ( ) ) . data ?. total ;
222
+ if ( total === undefined ) {
223
+ // No cache exists for this board, so we can't update it.
224
+ continue ;
225
+ }
226
+ entries . push ( {
227
+ endpointName : 'getBoardImagesTotal' ,
228
+ arg : boardId ,
229
+ value : { total : total + amountToAdd } ,
230
+ } ) ;
231
+ }
232
+ dispatch ( boardsApi . util . upsertQueryEntries ( entries ) ) ;
233
+
234
+ dispatch (
235
+ boardsApi . util . updateQueryData ( 'listAllBoards' , selectListBoardsQueryArgs ( getState ( ) ) , ( draft ) => {
236
+ for ( const board of draft ) {
237
+ board . image_count = board . image_count + ( boardTotalAdditions [ board . board_id ] ?? 0 ) ;
238
+ }
239
+ } )
240
+ ) ;
241
+
242
+ /**
243
+ * Optimistic update and cache invalidation for image names queries that match this image's board and categories.
244
+ * - Optimistic update for the cache that does not have a search term (we cannot derive the correct insertion
245
+ * position when a search term is present).
246
+ * - Cache invalidation for the query that has a search term, so it will be refetched.
247
+ *
248
+ * Note: The image DTO objects are already implicitly cached by the getResultImageDTOs function. We do not need
249
+ * to explicitly cache them again here.
250
+ */
251
+ for ( const videoDTO of videoDTOs ) {
252
+ // Override board_id and categories for this specific image to build the "expected" args for the query.
253
+ const videoSpecificArgs = {
254
+ board_id : videoDTO . board_id ?? 'none' ,
255
+ } ;
256
+
257
+ const expectedQueryArgs = {
258
+ ...getVideoIdsArg ,
259
+ ...videoSpecificArgs ,
260
+ search_term : '' ,
261
+ } ;
262
+
263
+ // If the cache for the query args provided here does not exist, RTK Query will ignore the update.
264
+ dispatch (
265
+ videosApi . util . updateQueryData (
266
+ 'getVideoIds' ,
267
+ {
268
+ ...getVideoIdsArg ,
269
+ ...videoSpecificArgs ,
270
+ search_term : '' ,
271
+ } ,
272
+ ( draft ) => {
273
+ const updatedResult = insertVideoIntoGetVideoIdsResult (
274
+ draft ,
275
+ videoDTO ,
276
+ expectedQueryArgs . starred_first ?? true ,
277
+ expectedQueryArgs . order_dir
278
+ ) ;
279
+
280
+ draft . video_ids = updatedResult . video_ids ;
281
+ draft . starred_count = updatedResult . starred_count ;
282
+ draft . total_count = updatedResult . total_count ;
283
+ }
284
+ )
285
+ ) ;
286
+
287
+ // If there is a search term present, we need to invalidate that query to ensure the search results are updated.
288
+ if ( getVideoIdsArg . search_term ) {
289
+ const expectedQueryArgs = {
290
+ ...getVideoIdsArg ,
291
+ ...videoSpecificArgs ,
292
+ } ;
293
+ dispatch ( imagesApi . util . invalidateTags ( [ { type : 'ImageNameList' , id : stableHash ( expectedQueryArgs ) } ] ) ) ;
294
+ }
295
+ }
296
+
297
+ // No need to invalidate tags since we're doing optimistic updates
298
+ // Board totals are already updated above via upsertQueryEntries
299
+
300
+ const autoSwitch = selectAutoSwitch ( getState ( ) ) ;
301
+
302
+ if ( ! autoSwitch ) {
303
+ return ;
304
+ }
305
+
306
+ // Finally, we may need to autoswitch to the new image. We'll only do it for the last image in the list.
307
+ const lastVideoDTO = videoDTOs . at ( - 1 ) ;
308
+
309
+ if ( ! lastVideoDTO ) {
310
+ return ;
311
+ }
312
+
313
+ const { video_id } = lastVideoDTO ;
314
+ const board_id = lastVideoDTO . board_id ?? 'none' ;
315
+
316
+ // With optimistic updates, we can immediately switch to the new image
317
+ const selectedBoardId = selectSelectedBoardId ( getState ( ) ) ;
318
+
319
+ // If the image is from a different board, switch to that board & select the image - otherwise just select the
320
+ // image. This implicitly changes the view to 'images' if it was not already.
321
+ if ( board_id !== selectedBoardId ) {
322
+ dispatch (
323
+ boardIdSelected ( {
324
+ boardId : board_id ,
325
+ selectedImageName : video_id ,
326
+ } )
327
+ ) ;
328
+ } else {
329
+ // Ensure we are on the 'images' gallery view - that's where this image will be displayed
330
+ const galleryView = selectGalleryView ( getState ( ) ) ;
331
+ if ( galleryView !== 'videos' ) {
332
+ dispatch ( galleryViewChanged ( 'videos' ) ) ;
333
+ }
334
+ // Select the image immediately since we've optimistically updated the cache
335
+ dispatch ( imageSelected ( lastVideoDTO . video_id ) ) ;
336
+ }
337
+ } ;
188
338
const getResultImageDTOs = async ( data : S [ 'InvocationCompleteEvent' ] ) : Promise < ImageDTO [ ] > => {
189
339
const { result } = data ;
190
340
const imageDTOs : ImageDTO [ ] = [ ] ;
@@ -206,17 +356,20 @@ export const buildOnInvocationComplete = (
206
356
return imageDTOs ;
207
357
} ;
208
358
209
- const getResultVideoFields = ( data : S [ 'InvocationCompleteEvent' ] ) : VideoField [ ] => {
359
+ const getResultVideoDTOs = async ( data : S [ 'InvocationCompleteEvent' ] ) : Promise < VideoDTO [ ] > => {
210
360
const { result } = data ;
211
- const videoFields : VideoField [ ] = [ ] ;
361
+ const videoDTOs : VideoDTO [ ] = [ ] ;
212
362
213
363
for ( const [ _name , value ] of objectEntries ( result ) ) {
214
364
if ( isVideoField ( value ) ) {
215
- videoFields . push ( value ) ;
365
+ const videoDTO = await getVideoDTOSafe ( value . video_id ) ;
366
+ if ( videoDTO ) {
367
+ videoDTOs . push ( videoDTO ) ;
368
+ }
216
369
}
217
370
}
218
371
219
- return videoFields ;
372
+ return videoDTOs ;
220
373
} ;
221
374
222
375
return async ( data : S [ 'InvocationCompleteEvent' ] ) => {
@@ -239,11 +392,7 @@ export const buildOnInvocationComplete = (
239
392
}
240
393
241
394
await addImagesToGallery ( data ) ;
242
-
243
- const videoField = getResultVideoFields ( data ) [ 0 ] ;
244
- if ( videoField ) {
245
- dispatch ( generatedVideoChanged ( { videoField } ) ) ;
246
- }
395
+ await addVideosToGallery ( data ) ;
247
396
248
397
$lastProgressEvent . set ( null ) ;
249
398
} ;
0 commit comments