feat(ui): rework progress event handling

- Canvas manages its own progress socket event listeners and progress event data.
- Remove cancellations listener jank.
- Dip into low-level redux subscription API to watch for queue status changes, clearing the last "global" progress event when the queue has nothing in progress. Could also do this in a useEffect I guess.
- Had to shuffle some things around to prevent circular imports, so there are a lot of tiny changes here.
This commit is contained in:
psychedelicious
2024-09-17 22:42:53 +10:00
committed by Kent Keirsey
parent b08a66ecaf
commit 7db4d26837
40 changed files with 161 additions and 305 deletions

View File

@@ -1,7 +1,6 @@
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
@@ -16,6 +15,7 @@ import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
type Props = {
image: ImageWithDims | null;

View File

@@ -1,5 +1,4 @@
import { useStore } from '@nanostores/react';
import { $socket } from 'app/hooks/useSocketIO';
import { logger } from 'app/logging/logger';
import { useAppStore } from 'app/store/nanostores/store';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
@@ -7,6 +6,7 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
import Konva from 'konva';
import { useLayoutEffect, useState } from 'react';
import { $socket } from 'services/events/stores';
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
const log = logger('canvas');

View File

@@ -1,4 +1,3 @@
import type { AppSocket } from 'app/hooks/useSocketIO';
import { logger } from 'app/logging/logger';
import type { AppStore } from 'app/store/store';
import type { SerializableObject } from 'common/types';
@@ -31,6 +30,7 @@ import Konva from 'konva';
import type { Atom } from 'nanostores';
import { computed } from 'nanostores';
import type { Logger } from 'roarr';
import type { AppSocket } from 'services/events/types';
import { assert } from 'tsafe';
import { CanvasBackgroundModule } from './CanvasBackgroundModule';

View File

@@ -4,7 +4,10 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
import { selectShowProgressOnCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
import Konva from 'konva';
import { atom } from 'nanostores';
import type { Logger } from 'roarr';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
import type { S } from 'services/api/types';
export class CanvasProgressImageModule extends CanvasModuleBase {
readonly type = 'progress_image';
@@ -23,7 +26,8 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
imageElement: HTMLImageElement | null = null;
subscriptions = new Set<() => void>();
$lastProgressEvent = atom<S['InvocationDenoiseProgressEvent'] | null>(null);
hasActiveGeneration: boolean = false;
mutex: Mutex = new Mutex();
constructor(manager: CanvasManager) {
@@ -41,11 +45,50 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
image: null,
};
this.subscriptions.add(this.manager.stateApi.$lastCanvasProgressEvent.listen(this.render));
this.subscriptions.add(this.manager.stagingArea.$shouldShowStagedImage.listen(this.render));
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectShowProgressOnCanvas, this.render));
this.subscriptions.add(this.setSocketEventListeners());
this.subscriptions.add(
this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => {
if (data && (data.in_progress > 0 || data.pending > 0)) {
this.hasActiveGeneration = true;
} else {
this.hasActiveGeneration = false;
this.$lastProgressEvent.set(null);
}
})
);
this.subscriptions.add(this.$lastProgressEvent.listen(this.render));
}
setSocketEventListeners = (): (() => void) => {
const progressListener = (data: S['InvocationDenoiseProgressEvent']) => {
if (data.destination !== 'canvas') {
return;
}
if (!this.hasActiveGeneration) {
return;
}
this.$lastProgressEvent.set(data);
};
const clearProgress = () => {
this.$lastProgressEvent.set(null);
};
this.manager.socket.on('invocation_denoise_progress', progressListener);
this.manager.socket.on('connect', clearProgress);
this.manager.socket.on('connect_error', clearProgress);
this.manager.socket.on('disconnect', clearProgress);
return () => {
this.manager.socket.off('invocation_denoise_progress', progressListener);
this.manager.socket.off('connect', clearProgress);
this.manager.socket.off('connect_error', clearProgress);
this.manager.socket.off('disconnect', clearProgress);
};
};
getNodes = () => {
return [this.konva.group];
};
@@ -53,7 +96,7 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
render = async () => {
const release = await this.mutex.acquire();
const event = this.manager.stateApi.$lastCanvasProgressEvent.get();
const event = this.$lastProgressEvent.get();
const showProgressOnCanvas = this.manager.stateApi.runSelector(selectShowProgressOnCanvas);
if (!event || !showProgressOnCanvas) {

View File

@@ -96,7 +96,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
if (!this.image.isLoading && !this.image.isError) {
await this.image.update({ ...this.image.state, image: imageDTOToImageWithDims(imageDTO) }, true);
this.manager.stateApi.$lastCanvasProgressEvent.set(null);
this.manager.progressImage.$lastProgressEvent.set(null);
}
this.image.konva.group.visible(shouldShowStagedImage);
} else {

View File

@@ -15,7 +15,6 @@ import {
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import {
$lastCanvasProgressEvent,
bboxChangedFromCanvas,
entityBrushLineAdded,
entityEraserLineAdded,
@@ -382,12 +381,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
*/
$isRasterizing = computed(this.$rasterizingAdapter, (rasterizingAdapter) => Boolean(rasterizingAdapter));
/**
* The last canvas progress event. This is set in a global event listener. The staging area may set it to null when it
* consumes the event.
*/
$lastCanvasProgressEvent = $lastCanvasProgressEvent;
/**
* Whether the space key is currently pressed.
*/

View File

@@ -30,13 +30,7 @@ import type { IRect } from 'konva/lib/types';
import { merge, omit } from 'lodash-es';
import { atom } from 'nanostores';
import type { UndoableOptions } from 'redux-undo';
import type {
ControlNetModelConfig,
ImageDTO,
IPAdapterModelConfig,
S,
T2IAdapterModelConfig,
} from 'services/api/types';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import type {
@@ -1236,7 +1230,6 @@ function actionsThrottlingFilter(action: UnknownAction) {
return true;
}
export const $lastCanvasProgressEvent = atom<S['InvocationDenoiseProgressEvent'] | null>(null);
/**
* The global canvas manager instance.
*/

View File

@@ -1,12 +1,12 @@
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppSelector } from 'app/store/storeHooks';
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { $isConnected } from 'services/events/stores';
type DeleteImageButtonProps = Omit<IconButtonProps, 'aria-label'> & {
onClick: () => void;

View File

@@ -1,7 +1,6 @@
import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { $isConnected } from 'app/hooks/useSocketIO';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
@@ -30,7 +29,7 @@ import {
PiRulerBold,
} from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { $progressImage } from 'services/events/setEventListeners';
import { $isConnected, $progressImage } from 'services/events/stores';
const CurrentImageButtons = () => {
const dispatch = useAppDispatch();

View File

@@ -15,7 +15,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { $hasProgress, $isProgressFromCanvas } from 'services/events/setEventListeners';
import { $hasProgress, $isProgressFromCanvas } from 'services/events/stores';
import ProgressImage from './ProgressImage';

View File

@@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useMemo } from 'react';
import { $isProgressFromCanvas, $progressImage } from 'services/events/setEventListeners';
import { $isProgressFromCanvas, $progressImage } from 'services/events/stores';
const selectShouldAntialiasProgressImage = createSelector(
selectSystemSlice,

View File

@@ -13,7 +13,7 @@ import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { NodeProps } from 'reactflow';
import { $lastProgressEvent } from 'services/events/setEventListeners';
import { $lastProgressEvent } from 'services/events/stores';
const CurrentImageNode = (props: NodeProps) => {
const imageDTO = useAppSelector(selectLastSelectedImage);

View File

@@ -1,7 +1,6 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
@@ -13,6 +12,7 @@ import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { PostUploadAction } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
import type { FieldComponentProps } from './types';

View File

@@ -11,7 +11,7 @@ export const prepareLinearUIBatch = (
prepend: boolean,
noise: Invocation<'noise' | 'flux_denoise'>,
posCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'>,
origin: 'generation' | 'workflows' | 'upscaling',
origin: 'canvas' | 'workflows' | 'upscaling',
destination: 'canvas' | 'gallery'
): BatchConfig => {
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;

View File

@@ -1,6 +1,5 @@
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
@@ -8,6 +7,7 @@ import { toast } from 'features/toast/toast';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelByBatchIdsMutation, useGetBatchStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelBatch = (batch_id: string) => {
const isConnected = useStore($isConnected);

View File

@@ -1,10 +1,10 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelQueueItemMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelCurrentQueueItem = () => {
const isConnected = useStore($isConnected);

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelQueueItemMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelQueueItem = (item_id: number) => {
const isConnected = useStore($isConnected);

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
export const useClearInvocationCache = () => {
const { t } = useTranslation();

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDisableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
export const useDisableInvocationCache = () => {
const { t } = useTranslation();

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
export const useEnableInvocationCache = () => {
const { t } = useTranslation();

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const usePauseProcessor = () => {
const { t } = useTranslation();

View File

@@ -1,11 +1,11 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePruneQueueMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const usePruneQueue = () => {
const dispatch = useAppDispatch();

View File

@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useResumeProcessor = () => {
const isConnected = useStore($isConnected);

View File

@@ -1,11 +1,10 @@
import { Progress } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $lastProgressEvent } from 'services/events/setEventListeners';
import { $isConnected, $lastProgressEvent } from 'services/events/stores';
const ProgressBar = () => {
const { t } = useTranslation();

View File

@@ -1,9 +1,9 @@
import { Icon, Tooltip } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiWarningBold } from 'react-icons/pi';
import { $isConnected } from 'services/events/stores';
const StatusIndicator = () => {
const isConnected = useStore($isConnected);