Merge branch 'main' into release/make-web-dist-startable

This commit is contained in:
psychedelicious 2023-05-26 18:06:38 +10:00 committed by GitHub
commit 582f516fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 647 additions and 302 deletions

View File

@ -8,8 +8,16 @@ import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit';
import type { RootState, AppDispatch } from '../../store';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addImageUploadedListener } from './listeners/imageUploaded';
import { addRequestedImageDeletionListener } from './listeners/imageDeleted';
import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
} from './listeners/imageUploaded';
import {
addImageDeletedFulfilledListener,
addImageDeletedPendingListener,
addImageDeletedRejectedListener,
addRequestedImageDeletionListener,
} from './listeners/imageDeleted';
import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
@ -28,6 +36,37 @@ import { addSocketDisconnectedListener } from './listeners/socketio/socketDiscon
import { addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
import { addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
import { addSessionReadyToInvokeListener } from './listeners/sessionReadyToInvoke';
import {
addImageMetadataReceivedFulfilledListener,
addImageMetadataReceivedRejectedListener,
} from './listeners/imageMetadataReceived';
import {
addImageUrlsReceivedFulfilledListener,
addImageUrlsReceivedRejectedListener,
} from './listeners/imageUrlsReceived';
import {
addSessionCreatedFulfilledListener,
addSessionCreatedPendingListener,
addSessionCreatedRejectedListener,
} from './listeners/sessionCreated';
import {
addSessionInvokedFulfilledListener,
addSessionInvokedPendingListener,
addSessionInvokedRejectedListener,
} from './listeners/sessionInvoked';
import {
addSessionCanceledFulfilledListener,
addSessionCanceledPendingListener,
addSessionCanceledRejectedListener,
} from './listeners/sessionCanceled';
import {
addReceivedResultImagesPageFulfilledListener,
addReceivedResultImagesPageRejectedListener,
} from './listeners/receivedResultImagesPage';
import {
addReceivedUploadImagesPageFulfilledListener,
addReceivedUploadImagesPageRejectedListener,
} from './listeners/receivedUploadImagesPage';
export const listenerMiddleware = createListenerMiddleware();
@ -47,23 +86,40 @@ export type AppListenerEffect = ListenerEffect<
AppDispatch
>;
addImageUploadedListener();
addInitialImageSelectedListener();
addRequestedImageDeletionListener();
// Image uploaded
addImageUploadedFulfilledListener();
addImageUploadedRejectedListener();
addInitialImageSelectedListener();
// Image deleted
addRequestedImageDeletionListener();
addImageDeletedPendingListener();
addImageDeletedFulfilledListener();
addImageDeletedRejectedListener();
// Image metadata
addImageMetadataReceivedFulfilledListener();
addImageMetadataReceivedRejectedListener();
// Image URLs
addImageUrlsReceivedFulfilledListener();
addImageUrlsReceivedRejectedListener();
// User Invoked
addUserInvokedCanvasListener();
addUserInvokedNodesListener();
addUserInvokedTextToImageListener();
addUserInvokedImageToImageListener();
addSessionReadyToInvokeListener();
// Canvas actions
addCanvasSavedToGalleryListener();
addCanvasDownloadedAsImageListener();
addCanvasCopiedToClipboardListener();
addCanvasMergedListener();
// socketio
addGeneratorProgressListener();
addGraphExecutionStateCompleteListener();
addInvocationCompleteListener();
@ -73,3 +129,24 @@ addSocketConnectedListener();
addSocketDisconnectedListener();
addSocketSubscribedListener();
addSocketUnsubscribedListener();
// Session Created
addSessionCreatedPendingListener();
addSessionCreatedFulfilledListener();
addSessionCreatedRejectedListener();
// Session Invoked
addSessionInvokedPendingListener();
addSessionInvokedFulfilledListener();
addSessionInvokedRejectedListener();
// Session Canceled
addSessionCanceledPendingListener();
addSessionCanceledFulfilledListener();
addSessionCanceledRejectedListener();
// Gallery pages
addReceivedResultImagesPageFulfilledListener();
addReceivedResultImagesPageRejectedListener();
addReceivedUploadImagesPageFulfilledListener();
addReceivedUploadImagesPageRejectedListener();

View File

@ -52,7 +52,6 @@ export const addCanvasMergedListener = () => {
dispatch(
imageUploaded({
imageType: 'intermediates',
formData: {
file: new File([blob], filename, { type: 'image/png' }),
},
@ -65,7 +64,7 @@ export const addCanvasMergedListener = () => {
action.meta.arg.formData.file.name === filename
);
const mergedCanvasImage = payload.response;
const mergedCanvasImage = payload;
dispatch(
setMergedCanvas({

View File

@ -29,7 +29,6 @@ export const addCanvasSavedToGalleryListener = () => {
dispatch(
imageUploaded({
imageType: 'results',
formData: {
file: new File([blob], 'mergedCanvas.png', { type: 'image/png' }),
},

View File

@ -4,9 +4,14 @@ import { imageDeleted } from 'services/thunks/image';
import { log } from 'app/logging/useLogger';
import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { uploadsAdapter } from 'features/gallery/store/uploadsSlice';
import { resultsAdapter } from 'features/gallery/store/resultsSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
/**
* Called when the user requests an image deletion
*/
export const addRequestedImageDeletionListener = () => {
startAppListening({
actionCreator: requestedImageDeletion,
@ -19,11 +24,6 @@ export const addRequestedImageDeletionListener = () => {
const { image_name, image_type } = image;
if (image_type !== 'uploads' && image_type !== 'results') {
moduleLog.warn({ data: image }, `Invalid image type ${image_type}`);
return;
}
const selectedImageName = getState().gallery.selectedImage?.image_name;
if (selectedImageName === image_name) {
@ -57,3 +57,49 @@ export const addRequestedImageDeletionListener = () => {
},
});
};
/**
* Called when the actual delete request is sent to the server
*/
export const addImageDeletedPendingListener = () => {
startAppListening({
actionCreator: imageDeleted.pending,
effect: (action, { dispatch, getState }) => {
const { imageName, imageType } = action.meta.arg;
// Preemptively remove the image from the gallery
if (imageType === 'uploads') {
uploadsAdapter.removeOne(getState().uploads, imageName);
}
if (imageType === 'results') {
resultsAdapter.removeOne(getState().results, imageName);
}
},
});
};
/**
* Called on successful delete
*/
export const addImageDeletedFulfilledListener = () => {
startAppListening({
actionCreator: imageDeleted.fulfilled,
effect: (action, { dispatch, getState }) => {
moduleLog.debug({ data: { image: action.meta.arg } }, 'Image deleted');
},
});
};
/**
* Called on failed delete
*/
export const addImageDeletedRejectedListener = () => {
startAppListening({
actionCreator: imageDeleted.rejected,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
{ data: { image: action.meta.arg } },
'Unable to delete image'
);
},
});
};

View File

@ -0,0 +1,43 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/thunks/image';
import {
ResultsImageDTO,
resultUpserted,
} from 'features/gallery/store/resultsSlice';
import {
UploadsImageDTO,
uploadUpserted,
} from 'features/gallery/store/uploadsSlice';
const moduleLog = log.child({ namespace: 'image' });
export const addImageMetadataReceivedFulfilledListener = () => {
startAppListening({
actionCreator: imageMetadataReceived.fulfilled,
effect: (action, { getState, dispatch }) => {
const image = action.payload;
moduleLog.debug({ data: { image } }, 'Image metadata received');
if (image.image_type === 'results') {
dispatch(resultUpserted(action.payload as ResultsImageDTO));
}
if (image.image_type === 'uploads') {
dispatch(uploadUpserted(action.payload as UploadsImageDTO));
}
},
});
};
export const addImageMetadataReceivedRejectedListener = () => {
startAppListening({
actionCreator: imageMetadataReceived.rejected,
effect: (action, { getState, dispatch }) => {
moduleLog.debug(
{ data: { image: action.meta.arg } },
'Problem receiving image metadata'
);
},
});
};

View File

@ -1,25 +1,31 @@
import { startAppListening } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
import { initialImageSelected } from 'features/parameters/store/actions';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import { resultUpserted } from 'features/gallery/store/resultsSlice';
import { isResultsImageDTO, isUploadsImageDTO } from 'services/types/guards';
import { log } from 'app/logging/useLogger';
export const addImageUploadedListener = () => {
const moduleLog = log.child({ namespace: 'image' });
export const addImageUploadedFulfilledListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(action) &&
action.payload.response.is_intermediate === false,
action.payload.is_intermediate === false,
effect: (action, { dispatch, getState }) => {
const { response: image } = action.payload;
const image = action.payload;
moduleLog.debug({ arg: '<Blob>', image }, 'Image uploaded');
const state = getState();
// Handle uploads
if (isUploadsImageDTO(image)) {
dispatch(uploadAdded(image));
dispatch(uploadUpserted(image));
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
@ -36,9 +42,26 @@ export const addImageUploadedListener = () => {
}
}
// Handle results
// TODO: Can this ever happen? I don't think so...
if (isResultsImageDTO(image)) {
dispatch(resultAdded(image));
dispatch(resultUpserted(image));
}
},
});
};
export const addImageUploadedRejectedListener = () => {
startAppListening({
actionCreator: imageUploaded.rejected,
effect: (action, { dispatch }) => {
dispatch(
addToast({
title: 'Image Upload Failed',
description: action.error.message,
status: 'error',
})
);
},
});
};

View File

@ -0,0 +1,51 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageUrlsReceived } from 'services/thunks/image';
import { resultsAdapter } from 'features/gallery/store/resultsSlice';
import { uploadsAdapter } from 'features/gallery/store/uploadsSlice';
const moduleLog = log.child({ namespace: 'image' });
export const addImageUrlsReceivedFulfilledListener = () => {
startAppListening({
actionCreator: imageUrlsReceived.fulfilled,
effect: (action, { getState, dispatch }) => {
const image = action.payload;
moduleLog.debug({ data: { image } }, 'Image URLs received');
const { image_type, image_name, image_url, thumbnail_url } = image;
if (image_type === 'results') {
resultsAdapter.updateOne(getState().results, {
id: image_name,
changes: {
image_url,
thumbnail_url,
},
});
}
if (image_type === 'uploads') {
uploadsAdapter.updateOne(getState().uploads, {
id: image_name,
changes: {
image_url,
thumbnail_url,
},
});
}
},
});
};
export const addImageUrlsReceivedRejectedListener = () => {
startAppListening({
actionCreator: imageUrlsReceived.rejected,
effect: (action, { getState, dispatch }) => {
moduleLog.debug(
{ data: { image: action.meta.arg } },
'Problem getting image URLs'
);
},
});
};

View File

@ -0,0 +1,33 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { receivedResultImagesPage } from 'services/thunks/gallery';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'gallery' });
export const addReceivedResultImagesPageFulfilledListener = () => {
startAppListening({
actionCreator: receivedResultImagesPage.fulfilled,
effect: (action, { getState, dispatch }) => {
const page = action.payload;
moduleLog.debug(
{ data: { page } },
`Received ${page.items.length} results`
);
},
});
};
export const addReceivedResultImagesPageRejectedListener = () => {
startAppListening({
actionCreator: receivedResultImagesPage.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
moduleLog.debug(
{ data: { error: serializeError(action.payload.error) } },
'Problem receiving results'
);
}
},
});
};

View File

@ -0,0 +1,33 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { receivedUploadImagesPage } from 'services/thunks/gallery';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'gallery' });
export const addReceivedUploadImagesPageFulfilledListener = () => {
startAppListening({
actionCreator: receivedUploadImagesPage.fulfilled,
effect: (action, { getState, dispatch }) => {
const page = action.payload;
moduleLog.debug(
{ data: { page } },
`Received ${page.items.length} uploads`
);
},
});
};
export const addReceivedUploadImagesPageRejectedListener = () => {
startAppListening({
actionCreator: receivedUploadImagesPage.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
moduleLog.debug(
{ data: { error: serializeError(action.payload.error) } },
'Problem receiving uploads'
);
}
},
});
};

View File

@ -0,0 +1,48 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { sessionCanceled } from 'services/thunks/session';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'session' });
export const addSessionCanceledPendingListener = () => {
startAppListening({
actionCreator: sessionCanceled.pending,
effect: (action, { getState, dispatch }) => {
//
},
});
};
export const addSessionCanceledFulfilledListener = () => {
startAppListening({
actionCreator: sessionCanceled.fulfilled,
effect: (action, { getState, dispatch }) => {
const { sessionId } = action.meta.arg;
moduleLog.debug(
{ data: { sessionId } },
`Session canceled (${sessionId})`
);
},
});
};
export const addSessionCanceledRejectedListener = () => {
startAppListening({
actionCreator: sessionCanceled.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
const { arg, error } = action.payload;
moduleLog.error(
{
data: {
arg,
error: serializeError(error),
},
},
`Problem canceling session`
);
}
},
});
};

View File

@ -0,0 +1,45 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { sessionCreated } from 'services/thunks/session';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'session' });
export const addSessionCreatedPendingListener = () => {
startAppListening({
actionCreator: sessionCreated.pending,
effect: (action, { getState, dispatch }) => {
//
},
});
};
export const addSessionCreatedFulfilledListener = () => {
startAppListening({
actionCreator: sessionCreated.fulfilled,
effect: (action, { getState, dispatch }) => {
const session = action.payload;
moduleLog.debug({ data: { session } }, `Session created (${session.id})`);
},
});
};
export const addSessionCreatedRejectedListener = () => {
startAppListening({
actionCreator: sessionCreated.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
const { arg, error } = action.payload;
moduleLog.error(
{
data: {
arg,
error: serializeError(error),
},
},
`Problem creating session`
);
}
},
});
};

View File

@ -0,0 +1,48 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { sessionInvoked } from 'services/thunks/session';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'session' });
export const addSessionInvokedPendingListener = () => {
startAppListening({
actionCreator: sessionInvoked.pending,
effect: (action, { getState, dispatch }) => {
//
},
});
};
export const addSessionInvokedFulfilledListener = () => {
startAppListening({
actionCreator: sessionInvoked.fulfilled,
effect: (action, { getState, dispatch }) => {
const { sessionId } = action.meta.arg;
moduleLog.debug(
{ data: { sessionId } },
`Session invoked (${sessionId})`
);
},
});
};
export const addSessionInvokedRejectedListener = () => {
startAppListening({
actionCreator: sessionInvoked.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
const { arg, error } = action.payload;
moduleLog.error(
{
data: {
arg,
error: serializeError(error),
},
},
`Problem invoking session`
);
}
},
});
};

View File

@ -3,7 +3,7 @@ import { sessionInvoked } from 'services/thunks/session';
import { log } from 'app/logging/useLogger';
import { sessionReadyToInvoke } from 'features/system/store/actions';
const moduleLog = log.child({ namespace: 'invoke' });
const moduleLog = log.child({ namespace: 'session' });
export const addSessionReadyToInvokeListener = () => {
startAppListening({
@ -11,7 +11,10 @@ export const addSessionReadyToInvokeListener = () => {
effect: (action, { getState, dispatch }) => {
const { sessionId } = getState().system;
if (sessionId) {
moduleLog.info({ sessionId }, `Session invoked (${sessionId})})`);
moduleLog.debug(
{ sessionId },
`Session ready to invoke (${sessionId})})`
);
dispatch(sessionInvoked({ sessionId }));
}
},

View File

@ -10,7 +10,7 @@ export const addGraphExecutionStateCompleteListener = () => {
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
action.payload,
`Graph execution state complete (${action.payload.data.graph_execution_state_id})`
`Session invocation complete (${action.payload.data.graph_execution_state_id})`
);
},
});

View File

@ -2,13 +2,11 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { invocationComplete } from 'services/events/actions';
import {
imageMetadataReceived,
imageUrlsReceived,
} from 'services/thunks/image';
import { imageMetadataReceived } from 'services/thunks/image';
import { sessionCanceled } from 'services/thunks/session';
import { isImageOutput } from 'services/types/guards';
import { progressImageSet } from 'features/system/store/systemSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
const moduleLog = log.child({ namespace: 'socketio' });
const nodeDenylist = ['dataURL_image'];
@ -17,7 +15,7 @@ export const addInvocationCompleteListener = () => {
startAppListening({
actionCreator: invocationComplete,
effect: async (action, { dispatch, getState, take }) => {
moduleLog.info(
moduleLog.debug(
action.payload,
`Invocation complete (${action.payload.data.node.type})`
);
@ -46,6 +44,14 @@ export const addInvocationCompleteListener = () => {
})
);
const [{ payload: imageDTO }] = await take(
imageMetadataReceived.fulfilled.match
);
if (getState().gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(imageDTO));
}
// Handle canvas image
if (
graph_execution_state_id ===

View File

@ -8,7 +8,7 @@ export const addInvocationErrorListener = () => {
startAppListening({
actionCreator: invocationError,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
moduleLog.error(
action.payload,
`Invocation error (${action.payload.data.node.type})`
);

View File

@ -19,7 +19,7 @@ export const addInvocationStartedListener = () => {
return;
}
moduleLog.info(
moduleLog.debug(
action.payload,
`Invocation started (${action.payload.data.node.type})`
);

View File

@ -91,7 +91,7 @@ export const addUserInvokedCanvasListener = () => {
dispatch(canvasGraphBuilt(graph));
moduleLog({ data: graph }, 'Canvas graph built');
moduleLog.debug({ data: graph }, 'Canvas graph built');
// If we are generating img2img or inpaint, we need to upload the init images
if (baseNode.type === 'img2img' || baseNode.type === 'inpaint') {
@ -106,19 +106,16 @@ export const addUserInvokedCanvasListener = () => {
);
// Wait for the image to be uploaded
const [{ payload: basePayload }] = await take(
const [{ payload: baseImageDTO }] = await take(
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(action) &&
action.meta.arg.formData.file.name === baseFilename
);
// Update the base node with the image name and type
const { image_name: baseName, image_type: baseType } =
basePayload.response;
baseNode.image = {
image_name: baseName,
image_type: baseType,
image_name: baseImageDTO.image_name,
image_type: baseImageDTO.image_type,
};
}
@ -135,19 +132,16 @@ export const addUserInvokedCanvasListener = () => {
);
// Wait for the mask to be uploaded
const [{ payload: maskPayload }] = await take(
const [{ payload: maskImageDTO }] = await take(
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(action) &&
action.meta.arg.formData.file.name === maskFilename
);
// Update the base node with the image name and type
const { image_name: maskName, image_type: maskType } =
maskPayload.response;
baseNode.mask = {
image_name: maskName,
image_type: maskType,
image_name: maskImageDTO.image_name,
image_type: maskImageDTO.image_type,
};
}

View File

@ -17,7 +17,7 @@ export const addUserInvokedImageToImageListener = () => {
const graph = buildImageToImageGraph(state);
dispatch(imageToImageGraphBuilt(graph));
moduleLog({ data: graph }, 'Image to Image graph built');
moduleLog.debug({ data: graph }, 'Image to Image graph built');
dispatch(sessionCreated({ graph }));

View File

@ -17,7 +17,7 @@ export const addUserInvokedNodesListener = () => {
const graph = buildNodesGraph(state);
dispatch(nodesGraphBuilt(graph));
moduleLog({ data: graph }, 'Nodes graph built');
moduleLog.debug({ data: graph }, 'Nodes graph built');
dispatch(sessionCreated({ graph }));

View File

@ -19,7 +19,7 @@ export const addUserInvokedTextToImageListener = () => {
dispatch(textToImageGraphBuilt(graph));
moduleLog({ data: graph }, 'Text to Image graph built');
moduleLog.debug({ data: graph }, 'Text to Image graph built');
dispatch(sessionCreated({ graph }));

View File

@ -68,7 +68,6 @@ const ImageUploader = (props: ImageUploaderProps) => {
async (file: File) => {
dispatch(
imageUploaded({
imageType: 'uploads',
formData: { file },
activeTabName,
})

View File

@ -1,14 +1,13 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import {
PayloadAction,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import {
receivedResultImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import {
imageDeleted,
imageMetadataReceived,
imageUrlsReceived,
} from 'services/thunks/image';
import { ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
@ -26,6 +25,7 @@ type AdditionalResultsState = {
pages: number;
isLoading: boolean;
nextPage: number;
upsertedImageCount: number;
};
export const initialResultsState =
@ -34,6 +34,7 @@ export const initialResultsState =
pages: 0,
isLoading: false,
nextPage: 0,
upsertedImageCount: 0,
});
export type ResultsState = typeof initialResultsState;
@ -42,7 +43,10 @@ const resultsSlice = createSlice({
name: 'results',
initialState: initialResultsState,
reducers: {
resultAdded: resultsAdapter.upsertOne,
resultUpserted: (state, action: PayloadAction<ResultsImageDTO>) => {
resultsAdapter.upsertOne(state, action.payload);
state.upsertedImageCount += 1;
},
},
extraReducers: (builder) => {
/**
@ -68,47 +72,6 @@ const resultsSlice = createSlice({
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
state.isLoading = false;
});
/**
* Image Metadata Received - FULFILLED
*/
builder.addCase(imageMetadataReceived.fulfilled, (state, action) => {
const { image_type } = action.payload;
if (image_type === 'results') {
resultsAdapter.upsertOne(state, action.payload as ResultsImageDTO);
}
});
/**
* Image URLs Received - FULFILLED
*/
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_type, image_url, thumbnail_url } =
action.payload;
if (image_type === 'results') {
resultsAdapter.updateOne(state, {
id: image_name,
changes: {
image_url: image_url,
thumbnail_url: thumbnail_url,
},
});
}
});
/**
* Delete Image - PENDING
* Pre-emptively remove the image from the gallery
*/
builder.addCase(imageDeleted.pending, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'results') {
resultsAdapter.removeOne(state, imageName);
}
});
},
});
@ -120,6 +83,6 @@ export const {
selectTotal: selectResultsTotal,
} = resultsAdapter.getSelectors<RootState>((state) => state.results);
export const { resultAdded } = resultsSlice.actions;
export const { resultUpserted } = resultsSlice.actions;
export default resultsSlice.reducer;

View File

@ -1,11 +1,14 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import {
PayloadAction,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import {
receivedUploadImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
import { ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
@ -23,6 +26,7 @@ type AdditionalUploadsState = {
pages: number;
isLoading: boolean;
nextPage: number;
upsertedImageCount: number;
};
export const initialUploadsState =
@ -31,6 +35,7 @@ export const initialUploadsState =
pages: 0,
nextPage: 0,
isLoading: false,
upsertedImageCount: 0,
});
export type UploadsState = typeof initialUploadsState;
@ -39,7 +44,10 @@ const uploadsSlice = createSlice({
name: 'uploads',
initialState: initialUploadsState,
reducers: {
uploadAdded: uploadsAdapter.upsertOne,
uploadUpserted: (state, action: PayloadAction<UploadsImageDTO>) => {
uploadsAdapter.upsertOne(state, action.payload);
state.upsertedImageCount += 1;
},
},
extraReducers: (builder) => {
/**
@ -65,36 +73,6 @@ const uploadsSlice = createSlice({
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
state.isLoading = false;
});
/**
* Image URLs Received - FULFILLED
*/
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_type, image_url, thumbnail_url } =
action.payload;
if (image_type === 'uploads') {
uploadsAdapter.updateOne(state, {
id: image_name,
changes: {
image_url: image_url,
thumbnail_url: thumbnail_url,
},
});
}
});
/**
* Delete Image - pending
* Pre-emptively remove the image from the gallery
*/
builder.addCase(imageDeleted.pending, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'uploads') {
uploadsAdapter.removeOne(state, imageName);
}
});
},
});
@ -106,6 +84,6 @@ export const {
selectTotal: selectUploadsTotal,
} = uploadsAdapter.getSelectors<RootState>((state) => state.uploads);
export const { uploadAdded } = uploadsSlice.actions;
export const { uploadUpserted } = uploadsSlice.actions;
export default uploadsSlice.reducer;

View File

@ -1,5 +1,5 @@
import { UseToastOptions } from '@chakra-ui/react';
import type { PayloadAction } from '@reduxjs/toolkit';
import { PayloadAction, isAnyOf } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import {
@ -16,7 +16,11 @@ import {
import { ProgressImage } from 'services/events/types';
import { makeToast } from '../../../app/components/Toaster';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import {
sessionCanceled,
sessionCreated,
sessionInvoked,
} from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
import { LogLevelName } from 'roarr';
@ -345,15 +349,8 @@ export const systemSlice = createSlice({
state.statusTranslationKey = 'common.statusPreparing';
});
builder.addCase(sessionInvoked.rejected, (state, action) => {
const error = action.payload as string | undefined;
state.toastQueue.push(
makeToast({ title: error || t('toast.serverError'), status: 'error' })
);
});
/**
* Session Canceled
* Session Canceled - FULFILLED
*/
builder.addCase(sessionCanceled.fulfilled, (state, action) => {
state.canceledSession = action.meta.arg.sessionId;
@ -416,6 +413,26 @@ export const systemSlice = createSlice({
builder.addCase(imageUploaded.fulfilled, (state) => {
state.isUploading = false;
});
// *** Matchers - must be after all cases ***
/**
* Session Invoked - REJECTED
* Session Created - REJECTED
*/
builder.addMatcher(isAnySessionRejected, (state, action) => {
state.isProcessing = false;
state.isCancelable = false;
state.isCancelScheduled = false;
state.currentStep = 0;
state.totalSteps = 0;
state.statusTranslationKey = 'common.statusConnected';
state.progressImage = null;
state.toastQueue.push(
makeToast({ title: t('toast.serverError'), status: 'error' })
);
});
},
});
@ -444,3 +461,8 @@ export const {
} = systemSlice.actions;
export default systemSlice.reducer;
const isAnySessionRejected = isAnyOf(
sessionCreated.rejected,
sessionInvoked.rejected
);

View File

@ -8,11 +8,7 @@ import {
import { socketSubscribed, socketUnsubscribed } from './actions';
import { AppThunkDispatch, RootState } from 'app/store/store';
import { getTimestamp } from 'common/util/getTimestamp';
import {
sessionInvoked,
sessionCreated,
sessionWithoutGraphCreated,
} from 'services/thunks/session';
import { sessionCreated } from 'services/thunks/session';
import { OpenAPI } from 'services/api';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { log } from 'app/logging/useLogger';
@ -66,10 +62,7 @@ export const socketMiddleware = () => {
socket.connect();
}
if (
sessionCreated.fulfilled.match(action) ||
sessionWithoutGraphCreated.fulfilled.match(action)
) {
if (sessionCreated.fulfilled.match(action)) {
const sessionId = action.payload.id;
const oldSessionId = getState().system.sessionId;

View File

@ -1,51 +1,64 @@
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { ImagesService } from 'services/api';
import { ImagesService, PaginatedResults_ImageDTO_ } from 'services/api';
export const IMAGES_PER_PAGE = 20;
const galleryLog = log.child({ namespace: 'gallery' });
type ReceivedResultImagesPageThunkConfig = {
rejectValue: {
error: unknown;
};
};
export const receivedResultImagesPage = createAppAsyncThunk(
export const receivedResultImagesPage = createAppAsyncThunk<
PaginatedResults_ImageDTO_,
void,
ReceivedResultImagesPageThunkConfig
>(
'results/receivedResultImagesPage',
async (_arg, { getState, rejectWithValue }) => {
const { page, pages, nextPage } = getState().results;
const { page, pages, nextPage, upsertedImageCount } = getState().results;
if (nextPage === page) {
return rejectWithValue([]);
}
// If many images have been upserted, we need to offset the page number
// TODO: add an offset param to the list images endpoint
const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
const response = await ImagesService.listImagesWithMetadata({
imageType: 'results',
imageCategory: 'general',
page: getState().results.nextPage,
page: nextPage + pageOffset,
perPage: IMAGES_PER_PAGE,
});
galleryLog.info({ response }, `Received ${response.items.length} results`);
return response;
}
);
export const receivedUploadImagesPage = createAppAsyncThunk(
type ReceivedUploadImagesPageThunkConfig = {
rejectValue: {
error: unknown;
};
};
export const receivedUploadImagesPage = createAppAsyncThunk<
PaginatedResults_ImageDTO_,
void,
ReceivedUploadImagesPageThunkConfig
>(
'uploads/receivedUploadImagesPage',
async (_arg, { getState, rejectWithValue }) => {
const { page, pages, nextPage } = getState().uploads;
const { page, pages, nextPage, upsertedImageCount } = getState().uploads;
if (nextPage === page) {
return rejectWithValue([]);
}
// If many images have been upserted, we need to offset the page number
// TODO: add an offset param to the list images endpoint
const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
const response = await ImagesService.listImagesWithMetadata({
imageType: 'uploads',
imageCategory: 'general',
page: getState().uploads.nextPage,
page: nextPage + pageOffset,
perPage: IMAGES_PER_PAGE,
});
galleryLog.info({ response }, `Received ${response.items.length} uploads`);
return response;
}
);

View File

@ -1,10 +1,6 @@
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { ImagesService } from 'services/api';
import { getHeaders } from 'services/util/getHeaders';
const imagesLog = log.child({ namespace: 'image' });
type imageUrlsReceivedArg = Parameters<
(typeof ImagesService)['getImageUrls']
@ -17,7 +13,6 @@ export const imageUrlsReceived = createAppAsyncThunk(
'api/imageUrlsReceived',
async (arg: imageUrlsReceivedArg) => {
const response = await ImagesService.getImageUrls(arg);
imagesLog.info({ arg, response }, 'Received image urls');
return response;
}
);
@ -33,7 +28,6 @@ export const imageMetadataReceived = createAppAsyncThunk(
'api/imageMetadataReceived',
async (arg: imageMetadataReceivedArg) => {
const response = await ImagesService.getImageMetadata(arg);
imagesLog.info({ arg, response }, 'Received image record');
return response;
}
);
@ -53,11 +47,7 @@ export const imageUploaded = createAppAsyncThunk(
// strip out `activeTabName` from arg - the route does not need it
const { activeTabName, ...rest } = arg;
const response = await ImagesService.uploadImage(rest);
const { location } = getHeaders(response);
imagesLog.debug({ arg: '<Blob>', response, location }, 'Image uploaded');
return { response, location };
return response;
}
);
@ -70,9 +60,6 @@ export const imageDeleted = createAppAsyncThunk(
'api/imageDeleted',
async (arg: ImageDeletedArg) => {
const response = await ImagesService.deleteImage(arg);
imagesLog.debug({ arg, response }, 'Image deleted');
return response;
}
);
@ -80,15 +67,12 @@ export const imageDeleted = createAppAsyncThunk(
type ImageUpdatedArg = Parameters<(typeof ImagesService)['updateImage']>[0];
/**
* `ImagesService.deleteImage()` thunk
* `ImagesService.updateImage()` thunk
*/
export const imageUpdated = createAppAsyncThunk(
'api/imageUpdated',
async (arg: ImageUpdatedArg) => {
const response = await ImagesService.updateImage(arg);
imagesLog.debug({ arg, response }, 'Image updated');
return response;
}
);

View File

@ -1,7 +1,7 @@
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { SessionsService } from 'services/api';
import { GraphExecutionState, SessionsService } from 'services/api';
import { log } from 'app/logging/useLogger';
import { serializeError } from 'serialize-error';
import { isObject } from 'lodash-es';
const sessionLog = log.child({ namespace: 'session' });
@ -11,144 +11,89 @@ type SessionCreatedArg = {
>[0]['requestBody'];
};
type SessionCreatedThunkConfig = {
rejectValue: { arg: SessionCreatedArg; error: unknown };
};
/**
* `SessionsService.createSession()` thunk
*/
export const sessionCreated = createAppAsyncThunk(
'api/sessionCreated',
async (arg: SessionCreatedArg, { rejectWithValue }) => {
try {
const response = await SessionsService.createSession({
requestBody: arg.graph,
});
sessionLog.info({ arg, response }, `Session created (${response.id})`);
return response;
} catch (err: any) {
sessionLog.error(
{
error: serializeError(err),
},
'Problem creating session'
);
return rejectWithValue(err.message);
}
}
);
/**
* `SessionsService.createSession()` without graph thunk
*/
export const sessionWithoutGraphCreated = createAppAsyncThunk(
'api/sessionWithoutGraphCreated',
async (_, { rejectWithValue }) => {
try {
const response = await SessionsService.createSession({});
sessionLog.info({ response }, `Session created (${response.id})`);
return response;
} catch (err: any) {
sessionLog.error(
{
error: serializeError(err),
},
'Problem creating session'
);
return rejectWithValue(err.message);
}
}
);
type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0];
/**
* `SessionsService.addNode()` thunk
*/
export const nodeAdded = createAppAsyncThunk(
'api/nodeAdded',
async (
arg: { node: NodeAddedArg['requestBody']; sessionId: string },
_thunkApi
) => {
const response = await SessionsService.addNode({
requestBody: arg.node,
sessionId: arg.sessionId,
export const sessionCreated = createAppAsyncThunk<
GraphExecutionState,
SessionCreatedArg,
SessionCreatedThunkConfig
>('api/sessionCreated', async (arg, { rejectWithValue }) => {
try {
const response = await SessionsService.createSession({
requestBody: arg.graph,
});
sessionLog.info({ arg, response }, `Node added (${response})`);
return response;
} catch (error) {
return rejectWithValue({ arg, error });
}
);
});
type NodeUpdatedArg = Parameters<(typeof SessionsService)['updateNode']>[0];
type SessionInvokedArg = { sessionId: string };
/**
* `SessionsService.addNode()` thunk
*/
export const nodeUpdated = createAppAsyncThunk(
'api/nodeUpdated',
async (
arg: { node: NodeUpdatedArg['requestBody']; sessionId: string },
_thunkApi
) => {
const response = await SessionsService.updateNode({
requestBody: arg.node,
sessionId: arg.sessionId,
nodePath: arg.node.id,
});
type SessionInvokedThunkConfig = {
rejectValue: {
arg: SessionInvokedArg;
error: unknown;
};
};
sessionLog.info({ arg, response }, `Node updated (${response})`);
return response;
}
);
const isErrorWithStatus = (error: unknown): error is { status: number } =>
isObject(error) && 'status' in error;
/**
* `SessionsService.invokeSession()` thunk
*/
export const sessionInvoked = createAppAsyncThunk(
'api/sessionInvoked',
async (arg: { sessionId: string }, { rejectWithValue }) => {
const { sessionId } = arg;
export const sessionInvoked = createAppAsyncThunk<
void,
SessionInvokedArg,
SessionInvokedThunkConfig
>('api/sessionInvoked', async (arg, { rejectWithValue }) => {
const { sessionId } = arg;
try {
const response = await SessionsService.invokeSession({
sessionId,
all: true,
});
sessionLog.info({ arg, response }, `Session invoked (${sessionId})`);
return response;
} catch (error) {
const err = error as any;
if (err.status === 403) {
return rejectWithValue(err.body.detail);
}
throw error;
try {
const response = await SessionsService.invokeSession({
sessionId,
all: true,
});
return response;
} catch (error) {
if (isErrorWithStatus(error) && error.status === 403) {
return rejectWithValue({ arg, error: (error as any).body.detail });
}
return rejectWithValue({ arg, error });
}
);
});
type SessionCanceledArg = Parameters<
(typeof SessionsService)['cancelSessionInvoke']
>[0];
type SessionCanceledThunkConfig = {
rejectValue: {
arg: SessionCanceledArg;
error: unknown;
};
};
/**
* `SessionsService.cancelSession()` thunk
*/
export const sessionCanceled = createAppAsyncThunk(
'api/sessionCanceled',
async (arg: SessionCanceledArg, _thunkApi) => {
const { sessionId } = arg;
export const sessionCanceled = createAppAsyncThunk<
void,
SessionCanceledArg,
SessionCanceledThunkConfig
>('api/sessionCanceled', async (arg: SessionCanceledArg, _thunkApi) => {
const { sessionId } = arg;
const response = await SessionsService.cancelSessionInvoke({
sessionId,
});
const response = await SessionsService.cancelSessionInvoke({
sessionId,
});
sessionLog.info({ arg, response }, `Session canceled (${sessionId})`);
return response;
}
);
return response;
});
type SessionsListedArg = Parameters<
(typeof SessionsService)['listSessions']