feat(ui): wip canvas migration, createListenerMiddleware

This commit is contained in:
psychedelicious 2023-05-03 19:27:29 +10:00
parent a75148cb16
commit 6ab5d28cf3
29 changed files with 352 additions and 175 deletions

View File

@ -118,6 +118,7 @@
"@types/node": "^18.16.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/react-redux": "^7.1.25",
"@types/react-transition-group": "^4.4.5",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",

View File

@ -550,7 +550,7 @@
"imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied",
"imageNotLoaded": "No Image Loaded",
"imageNotLoadedDesc": "No image found to send to image to image module",
"imageNotLoadedDesc": "Could not find image",
"imageSavedToGallery": "Image Saved to Gallery",
"canvasMerged": "Canvas Merged",
"sentToImageToImage": "Sent To Image To Image",

View File

@ -0,0 +1,59 @@
import {
createListenerMiddleware,
addListener,
ListenerEffect,
AnyAction,
} from '@reduxjs/toolkit';
import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit';
import type { RootState, AppDispatch } from '../../store';
import { initialImageSelected } from 'features/parameters/store/actions';
import { initialImageListener } from './listeners/initialImageListener';
import {
imageResultReceivedListener,
imageResultReceivedPrediate,
} from './listeners/invocationCompleteListener';
import { imageUploaded } from 'services/thunks/image';
import { imageUploadedListener } from './listeners/imageUploadedListener';
export const listenerMiddleware = createListenerMiddleware();
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
export const startAppListening =
listenerMiddleware.startListening as AppStartListening;
export const addAppListener = addListener as TypedAddListener<
RootState,
AppDispatch
>;
export type AppListenerEffect = ListenerEffect<
AnyAction,
RootState,
AppDispatch
>;
/**
* Initial image selected
*/
startAppListening({
actionCreator: initialImageSelected,
effect: initialImageListener,
});
/**
* Image Result received
*/
startAppListening({
predicate: imageResultReceivedPrediate,
effect: imageResultReceivedListener,
});
/**
* Image Uploaded
*/
startAppListening({
actionCreator: imageUploaded.fulfilled,
effect: imageUploadedListener,
});

View File

@ -0,0 +1,19 @@
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { AppListenerEffect } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
export const imageUploadedListener: AppListenerEffect = (
action,
{ dispatch, getState }
) => {
const { response } = action.payload;
const state = getState();
const image = deserializeImageResponse(response);
dispatch(uploadAdded(image));
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
};

View File

@ -0,0 +1,53 @@
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { Image, isInvokeAIImage } from 'app/types/invokeai';
import { selectResultsById } from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { makeToast } from 'features/system/hooks/useToastWatcher';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { AnyAction, ListenerEffect } from '@reduxjs/toolkit';
import { AppDispatch, RootState } from 'app/store/store';
export const initialImageListener: ListenerEffect<
AnyAction,
RootState,
AppDispatch
> = (action, { getState, dispatch }) => {
if (!action.payload) {
dispatch(
addToast(
makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
)
);
return;
}
if (isInvokeAIImage(action.payload)) {
dispatch(initialImageChanged(action.payload));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
return;
}
const { name, type } = action.payload;
let image: Image | undefined;
const state = getState();
if (type === 'results') {
image = selectResultsById(state, name);
} else if (type === 'uploads') {
image = selectUploadsById(state, name);
}
if (!image) {
dispatch(
addToast(
makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
)
);
return;
}
dispatch(initialImageChanged(image));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
};

View File

@ -0,0 +1,75 @@
import { AnyListenerPredicate } from '@reduxjs/toolkit';
import { invocationComplete } from 'services/events/actions';
import { isImageOutput } from 'services/types/guards';
import { RootState } from 'app/store/store';
import {
buildImageUrls,
extractTimestampFromImageName,
} from 'services/util/deserializeImageField';
import { Image } from 'app/types/invokeai';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import { AppListenerEffect } from '..';
export const imageResultReceivedPrediate: AnyListenerPredicate<RootState> = (
action,
_currentState,
_originalState
) => {
if (
invocationComplete.match(action) &&
isImageOutput(action.payload.data.result)
) {
return true;
}
return false;
};
export const imageResultReceivedListener: AppListenerEffect = (
action,
{ getState, dispatch }
) => {
const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) {
const name = result.image.image_name;
const type = result.image.image_type;
const state = getState();
// if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name);
const image: Image = {
name,
type,
url,
thumbnail,
metadata: {
created: timestamp,
width: result.width,
height: result.height,
invokeai: {
session_id: graph_execution_state_id,
...(node ? { node } : {}),
},
},
};
dispatch(resultAdded(image));
if (state.config.shouldFetchImages) {
dispatch(imageReceived({ imageName: name, imageType: type }));
dispatch(
thumbnailReceived({
thumbnailName: name,
thumbnailType: type,
})
);
}
}
};

View File

@ -1,4 +1,9 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import {
AnyAction,
ThunkDispatch,
combineReducers,
configureStore,
} from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
@ -30,6 +35,7 @@ import { systemDenylist } from 'features/system/store/systemPersistDenylist';
import { uiDenylist } from 'features/ui/store/uiPersistDenylist';
import { resultsDenylist } from 'features/gallery/store/resultsPersistDenylist';
import { uploadsDenylist } from 'features/gallery/store/uploadsPersistDenylist';
import { listenerMiddleware } from './middleware/listenerMiddleware';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
@ -101,7 +107,9 @@ export const store = configureStore({
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(dynamicMiddlewares),
})
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [
@ -120,4 +128,5 @@ export const store = configureStore({
export type AppGetState = typeof store.getState;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunkDispatch = ThunkDispatch<RootState, any, AnyAction>;
export type AppDispatch = typeof store.dispatch;

View File

@ -1,6 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from 'app/store/store';
import { AppDispatch, AppThunkDispatch, RootState } from 'app/store/store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppDispatch = () => useDispatch<AppThunkDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -13,6 +13,7 @@
*/
import { GalleryCategory } from 'features/gallery/store/gallerySlice';
import { SelectedImage } from 'features/parameters/store/actions';
import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
@ -126,6 +127,14 @@ export type Image = {
metadata: ImageResponseMetadata;
};
export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => {
if ('url' in obj && 'thumbnail' in obj) {
return true;
}
return false;
};
/**
* Types related to the system status.
*/

View File

@ -29,7 +29,6 @@ import {
isCanvasBaseImage,
isCanvasMaskLine,
} from './canvasTypes';
import { invocationComplete } from 'services/events/actions';
export const initialLayerState: CanvasLayerState = {
objects: [],
@ -816,11 +815,6 @@ export const canvasSlice = createSlice({
state.isTransformingBoundingBox = false;
},
},
extraReducers(builder) {
builder.addCase(invocationComplete, (state, action) => {
//
});
},
});
export const {

View File

@ -22,7 +22,7 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings';
import {
initialImageSelected,
initialImageChanged,
setAllParameters,
// setInitialImage,
setSeed,
@ -68,6 +68,7 @@ import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { imageDeleted } from 'services/thunks/image';
import { useParameters } from 'features/parameters/hooks/useParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
const currentImageButtonsSelector = createSelector(
[
@ -264,8 +265,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys('p', handleUsePrompt, [image]);
const handleSendToImageToImage = useCallback(() => {
sendToImageToImage(image);
}, [image, sendToImageToImage]);
dispatch(initialImageSelected(image));
}, [dispatch, image]);
useHotkeys('shift+i', handleSendToImageToImage, [image]);

View File

@ -11,7 +11,7 @@ import CurrentImageFallback from './CurrentImageFallback';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons';
import CurrentImageHidden from './CurrentImageHidden';
import { memo } from 'react';
import { DragEvent, memo, useCallback } from 'react';
export const imagesSelector = createSelector(
[uiSelector, selectedImageSelector, systemSelector],
@ -36,6 +36,18 @@ const CurrentImagePreview = () => {
useAppSelector(imagesSelector);
const { getUrl } = useGetUrl();
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
if (!image) {
return;
}
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';
},
[image]
);
return (
<Flex
sx={{
@ -48,6 +60,7 @@ const CurrentImagePreview = () => {
>
{image && (
<Image
onDragStart={handleDragStart}
src={shouldHidePreview ? undefined : getUrl(image.url)}
width={image.metadata.width}
height={image.metadata.height}

View File

@ -134,7 +134,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
console.log('dragging');
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';

View File

@ -4,7 +4,7 @@ import { invocationComplete } from 'services/events/actions';
import { isImageOutput } from 'services/types/guards';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageUploaded } from 'services/thunks/image';
import { SelectedImage } from 'features/parameters/store/generationSlice';
import { Image } from 'app/types/invokeai';
type GalleryImageObjectFitType = 'contain' | 'cover';
@ -12,7 +12,7 @@ export interface GalleryState {
/**
* The selected image
*/
selectedImage?: SelectedImage;
selectedImage?: Image;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
@ -22,7 +22,6 @@ export interface GalleryState {
}
const initialState: GalleryState = {
selectedImage: undefined,
galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true,
@ -35,10 +34,7 @@ export const gallerySlice = createSlice({
name: 'gallery',
initialState,
reducers: {
imageSelected: (
state,
action: PayloadAction<SelectedImage | undefined>
) => {
imageSelected: (state, action: PayloadAction<Image | undefined>) => {
state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false;
@ -84,16 +80,6 @@ export const gallerySlice = createSlice({
};
}
});
/**
* Upload Image - FULFILLED
*/
builder.addCase(imageUploaded.fulfilled, (state, action) => {
const { response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
state.selectedImage = { name: uploadedImage.name, type: 'uploads' };
});
},
});

View File

@ -73,43 +73,43 @@ const resultsSlice = createSlice({
state.isLoading = false;
});
/**
* Invocation Complete
*/
builder.addCase(invocationComplete, (state, action) => {
const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data;
// /**
// * Invocation Complete
// */
// builder.addCase(invocationComplete, (state, action) => {
// const { data, shouldFetchImages } = action.payload;
// const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) {
const name = result.image.image_name;
const type = result.image.image_type;
// if (isImageOutput(result)) {
// const name = result.image.image_name;
// const type = result.image.image_type;
// if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
// // if we need to refetch, set URLs to placeholder for now
// const { url, thumbnail } = shouldFetchImages
// ? { url: '', thumbnail: '' }
// : buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name);
// const timestamp = extractTimestampFromImageName(name);
const image: Image = {
name,
type,
url,
thumbnail,
metadata: {
created: timestamp,
width: result.width,
height: result.height,
invokeai: {
session_id: graph_execution_state_id,
...(node ? { node } : {}),
},
},
};
// const image: Image = {
// name,
// type,
// url,
// thumbnail,
// metadata: {
// created: timestamp,
// width: result.width,
// height: result.height,
// invokeai: {
// session_id: graph_execution_state_id,
// ...(node ? { node } : {}),
// },
// },
// };
resultsAdapter.setOne(state, image);
}
});
// resultsAdapter.setOne(state, image);
// }
// });
/**
* Image Received - FULFILLED

View File

@ -35,7 +35,7 @@ const uploadsSlice = createSlice({
name: 'uploads',
initialState: initialUploadsState,
reducers: {
uploadAdded: uploadsAdapter.addOne,
uploadAdded: uploadsAdapter.upsertOne,
},
extraReducers: (builder) => {
/**
@ -61,17 +61,6 @@ const uploadsSlice = createSlice({
state.isLoading = false;
});
/**
* Upload Image - FULFILLED
*/
builder.addCase(imageUploaded.fulfilled, (state, action) => {
const { location, response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
uploadsAdapter.setOne(state, uploadedImage);
});
/**
* Delete Image - FULFILLED
*/

View File

@ -5,9 +5,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl';
import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName';
import {
import generationSlice, {
clearInitialImage,
initialImageSelected,
initialImageChanged,
} from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es';
@ -15,23 +15,26 @@ import { DragEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageType } from 'services/api';
import ImageToImageOverlay from 'common/components/ImageToImageOverlay';
import { initialImageSelector } from 'features/parameters/store/generationSelectors';
import {
generationSelector,
initialImageSelector,
} from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions';
const selector = createSelector(
[initialImageSelector],
(initialImage) => {
[generationSelector],
(generation) => {
const { initialImage, isImageToImageEnabled } = generation;
return {
initialImage,
isImageToImageEnabled,
};
},
{ memoizeOptions: { resultEqualityCheck: isEqual } }
);
const InitialImagePreview = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const { initialImage } = useAppSelector(selector);
const { initialImage, isImageToImageEnabled } = useAppSelector(selector);
const { getUrl } = useGetUrl();
const dispatch = useAppDispatch();
const { t } = useTranslation();
@ -55,22 +58,13 @@ const InitialImagePreview = () => {
const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
setIsLoaded(false);
const name = e.dataTransfer.getData('invokeai/imageName');
const type = e.dataTransfer.getData('invokeai/imageType') as ImageType;
if (!name || !type) {
return;
}
const image = getImageByNameAndType(name, type);
if (!image) {
return;
}
dispatch(initialImageSelected({ name, type }));
},
[getImageByNameAndType, dispatch]
[dispatch]
);
return (

View File

@ -4,9 +4,11 @@ import { isFinite, isString } from 'lodash-es';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useSetBothPrompts from './usePrompt';
import { initialImageSelected, setSeed } from '../store/generationSlice';
import { initialImageChanged, setSeed } from '../store/generationSlice';
import { isImage, isImageField } from 'services/types/guards';
import { NUMPY_RAND_MAX } from 'app/constants';
import { initialImageSelected } from '../store/actions';
import { Image } from 'app/types/invokeai';
export const useParameters = () => {
const dispatch = useAppDispatch();
@ -86,7 +88,7 @@ export const useParameters = () => {
}
dispatch(
initialImageSelected({ name: image.image_name, type: image.image_type })
initialImageChanged({ name: image.image_name, type: image.image_type })
);
toast({
title: t('toast.initialImageSet'),
@ -102,27 +104,10 @@ export const useParameters = () => {
* Sets image as initial image with toast
*/
const sendToImageToImage = useCallback(
(image: unknown) => {
if (!isImage(image)) {
toast({
title: t('toast.imageNotLoaded'),
description: t('toast.imageNotLoadedDesc'),
status: 'warning',
duration: 2500,
isClosable: true,
});
return;
}
(image: Image) => {
dispatch(initialImageSelected({ name: image.name, type: image.type }));
toast({
title: t('toast.sentToImageToImage'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
[t, toast, dispatch]
[dispatch]
);
return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage };

View File

@ -0,0 +1,12 @@
import { createAction } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai';
import { ImageType } from 'services/api';
export type SelectedImage = {
name: string;
type: ImageType;
};
export const initialImageSelected = createAction<
Image | SelectedImage | undefined
>('generation/initialImageSelected');

View File

@ -7,17 +7,12 @@ import { seedWeightsToString } from 'common/util/seedWeightPairs';
import { clamp } from 'lodash-es';
import { ImageField, ImageType } from 'services/api';
export type SelectedImage = {
name: string;
type: ImageType;
};
export interface GenerationState {
cfgScale: number;
height: number;
img2imgStrength: number;
infillMethod: string;
initialImage?: SelectedImage; // can be an Image or url
initialImage?: InvokeAI.Image; // can be an Image or url
iterations: number;
maskPath: string;
perlin: number;
@ -351,7 +346,7 @@ export const generationSlice = createSlice({
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
state.verticalSymmetrySteps = action.payload;
},
initialImageSelected: (state, action: PayloadAction<SelectedImage>) => {
initialImageChanged: (state, action: PayloadAction<InvokeAI.Image>) => {
state.initialImage = action.payload;
state.isImageToImageEnabled = true;
},
@ -399,7 +394,7 @@ export const {
setShouldUseSymmetry,
setHorizontalSymmetrySteps,
setVerticalSymmetrySteps,
initialImageSelected,
initialImageChanged,
isImageToImageEnabledChanged,
} = generationSlice.actions;

View File

@ -15,7 +15,7 @@ import {
} from 'services/events/actions';
import { ProgressImage } from 'services/events/types';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { makeToast } from '../hooks/useToastWatcher';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model';
@ -434,13 +434,6 @@ export const systemSlice = createSlice({
state.statusTranslationKey = 'common.statusConnected';
});
/**
* Initial Image Selected
*/
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(t('toast.sentToImageToImage')));
});
/**
* Received available models from the backend
*/

View File

@ -1,7 +1,7 @@
import { Box, BoxProps, Grid, GridItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import {
activeTabNameSelector,
uiSelector,

View File

@ -3,7 +3,7 @@
/* eslint-disable */
/**
* Outputs an image from a base 64 data URL.
* Outputs an image from a data URL.
*/
export type DataURLToImageInvocation = {
/**

View File

@ -2,7 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export const $DataURLToImageInvocation = {
description: `Outputs an image from a base 64 data URL.`,
description: `Outputs an image from a data URL.`,
properties: {
id: {
type: 'string',

View File

@ -1,4 +1,4 @@
import { createAction } from '@reduxjs/toolkit';
import { AnyAction, createAction } from '@reduxjs/toolkit';
import {
GeneratorProgressEvent,
GraphExecutionStateCompleteEvent,

View File

@ -5,20 +5,14 @@ import {
ClientToServerEvents,
ServerToClientEvents,
} from 'services/events/types';
import {
invocationComplete,
socketSubscribed,
socketUnsubscribed,
} from './actions';
import { AppDispatch, RootState } from 'app/store/store';
import { socketSubscribed, socketUnsubscribed } from './actions';
import { AppThunkDispatch, RootState } from 'app/store/store';
import { getTimestamp } from 'common/util/getTimestamp';
import {
sessionInvoked,
isFulfilledSessionCreatedAction,
} from 'services/thunks/session';
import { OpenAPI } from 'services/api';
import { isImageOutput } from 'services/types/guards';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { log } from 'app/logging/useLogger';
@ -56,13 +50,15 @@ export const socketMiddleware = () => {
);
const middleware: Middleware =
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
const { dispatch, getState } = store;
(storeApi: MiddlewareAPI<AppThunkDispatch, RootState>) =>
(next) =>
(action) => {
const { dispatch, getState } = storeApi;
// Set listeners for `connect` and `disconnect` events once
// Must happen in middleware to get access to `dispatch`
if (!areListenersSet) {
setEventListeners({ store, socket, log: socketioLog });
setEventListeners({ storeApi, socket, log: socketioLog });
areListenersSet = true;
@ -107,26 +103,6 @@ export const socketMiddleware = () => {
dispatch(sessionInvoked({ sessionId }));
}
if (invocationComplete.match(action)) {
const { config } = getState();
if (config.shouldFetchImages) {
const { result } = action.payload.data;
if (isImageOutput(result)) {
const imageName = result.image.image_name;
const imageType = result.image.image_type;
dispatch(imageReceived({ imageName, imageType }));
dispatch(
thumbnailReceived({
thumbnailName: imageName,
thumbnailType: imageType,
})
);
}
}
}
next(action);
};

View File

@ -27,13 +27,13 @@ import { addToast } from '../../../features/system/store/systemSlice';
type SetEventListenersArg = {
socket: Socket<ServerToClientEvents, ClientToServerEvents>;
store: MiddlewareAPI<AppDispatch, RootState>;
storeApi: MiddlewareAPI<AppDispatch, RootState>;
log: Logger<JsonObject>;
};
export const setEventListeners = (arg: SetEventListenersArg) => {
const { socket, store, log } = arg;
const { dispatch, getState } = store;
const { socket, storeApi, log } = arg;
const { dispatch, getState } = storeApi;
/**
* Connect

View File

@ -20,7 +20,12 @@
"*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "*.d.ts"],
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"*.d.ts",
"src/app/store/middleware/listenerMiddleware"
],
"exclude": ["src/services/fixtures/*", "node_modules", "dist"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -1836,7 +1836,7 @@
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
"@types/hoist-non-react-statics@^3.3.1":
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -1907,6 +1907,16 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.25":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-transition-group@^4.4.5":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
@ -5687,7 +5697,7 @@ redux-thunk@^2.4.2:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
redux@^4.2.1:
redux@^4.0.0, redux@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==