Skip to content

Commit 2472cd6

Browse files
feat(ui): api layer refactor
*migrate from `openapi-typescript-codegen` to `openapi-typescript` and `openapi-fetch`* `openapi-typescript-codegen` is not very actively maintained - it's been over a year since the last update. `openapi-typescript` and `openapi-fetch` are part of the actively maintained repo. key differences: - provides a `fetch` client instead of `axios`, which means we need to be a bit more verbose with typing thunks - fetch client is created at runtime and has a very nice typescript DX - generates a single file with all types in it, from which we then extract individual types. i don't like how verbose this is, but i do like how it is more explicit. - removed npm api generation scripts - now we have a single `typegen` script overall i have more confidence in this new library. *use nanostores for api base and token* very simple reactive store for api base url and token. this was suggested in the `openapi-fetch` docs and i quite like the strategy. *organise rtk-query api* split out each endpoint (models, images, boards, boardImages) into their own api extensions. tidy!
1 parent 2f6a4f6 commit 2472cd6

File tree

267 files changed

+6257
-7008
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

267 files changed

+6257
-7008
lines changed

invokeai/frontend/web/package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
"dev": "concurrently \"vite dev\" \"yarn run theme:watch\"",
2424
"dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"",
2525
"build": "yarn run lint && vite build",
26-
"api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --indent 2 --request src/services/fixtures/request.ts",
27-
"api:file": "openapi -i src/services/fixtures/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --indent 2 --request src/services/fixtures/request.ts",
26+
"typegen": "npx openapi-typescript http://localhost:9090/openapi.json --output src/services/schema.d.ts -t",
2827
"preview": "vite preview",
2928
"lint:madge": "madge --circular src/main.tsx",
3029
"lint:eslint": "eslint --max-warnings=0 .",
@@ -81,6 +80,8 @@
8180
"i18next-http-backend": "^2.2.0",
8281
"konva": "^9.0.1",
8382
"lodash-es": "^4.17.21",
83+
"nanostores": "^0.9.2",
84+
"openapi-fetch": "^0.4.0",
8485
"overlayscrollbars": "^2.1.1",
8586
"overlayscrollbars-react": "^0.5.0",
8687
"patch-package": "^7.0.0",
@@ -140,6 +141,7 @@
140141
"lint-staged": "^13.2.2",
141142
"madge": "^6.0.0",
142143
"openapi-types": "^12.1.0",
144+
"openapi-typescript": "^6.2.8",
143145
"openapi-typescript-codegen": "^0.24.0",
144146
"postinstall-postinstall": "^2.1.0",
145147
"prettier": "^2.8.8",

invokeai/frontend/web/src/app/components/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Toaster from './Toaster';
2424
import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
2525
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
2626
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
27-
import { useListModelsQuery } from 'services/apiSlice';
27+
import { useListModelsQuery } from 'services/api/endpoints/models';
2828

2929
const DEFAULT_CONFIG = {};
3030

invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
} from '@dnd-kit/core';
1212
import { PropsWithChildren, memo, useCallback, useState } from 'react';
1313
import OverlayDragImage from './OverlayDragImage';
14-
import { ImageDTO } from 'services/api';
15-
import { isImageDTO } from 'services/types/guards';
14+
import { ImageDTO } from 'services/api/types';
15+
import { isImageDTO } from 'services/api/guards';
1616
import { snapCenterToCursor } from '@dnd-kit/modifiers';
1717
import { AnimatePresence, motion } from 'framer-motion';
1818

invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Box, Image } from '@chakra-ui/react';
22
import { memo } from 'react';
3-
import { ImageDTO } from 'services/api';
3+
import { ImageDTO } from 'services/api/types';
44

55
type OverlayDragImageProps = {
66
image: ImageDTO;

invokeai/frontend/web/src/app/components/InvokeAIUI.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
} from 'react';
88
import { Provider } from 'react-redux';
99
import { store } from 'app/store/store';
10-
import { OpenAPI } from 'services/api';
10+
// import { OpenAPI } from 'services/api/types';
1111

1212
import Loading from '../../common/components/Loading/Loading';
1313
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
@@ -23,6 +23,7 @@ import {
2323
} from 'app/contexts/DeleteImageContext';
2424
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
2525
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
26+
import { $authToken, $baseUrl } from 'services/api/client';
2627

2728
const App = lazy(() => import('./App'));
2829
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@@ -47,12 +48,12 @@ const InvokeAIUI = ({
4748
useEffect(() => {
4849
// configure API client token
4950
if (token) {
50-
OpenAPI.TOKEN = token;
51+
$authToken.set(token);
5152
}
5253

5354
// configure API client base url
5455
if (apiUrl) {
55-
OpenAPI.BASE = apiUrl;
56+
$baseUrl.set(apiUrl);
5657
}
5758

5859
// reset dynamically added middlewares
@@ -69,6 +70,12 @@ const InvokeAIUI = ({
6970
} else {
7071
addMiddleware(socketMiddleware());
7172
}
73+
74+
return () => {
75+
// Reset the API client token and base url on unmount
76+
$baseUrl.set(undefined);
77+
$authToken.set(undefined);
78+
};
7279
}, [apiUrl, token, middleware]);
7380

7481
return (

invokeai/frontend/web/src/app/contexts/AddImageToBoardContext.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useDisclosure } from '@chakra-ui/react';
22
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
3-
import { ImageDTO } from 'services/api';
4-
import { useAddImageToBoardMutation } from 'services/apiSlice';
3+
import { ImageDTO } from 'services/api/types';
4+
import { useAddImageToBoardMutation } from 'services/api/endpoints/boardImages';
55

66
export type ImageUsage = {
77
isInitialImage: boolean;

invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
useEffect,
1212
useState,
1313
} from 'react';
14-
import { ImageDTO } from 'services/api';
14+
import { ImageDTO } from 'services/api/types';
1515
import { RootState } from 'app/store/store';
1616
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
1717
import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';

invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AnyAction } from '@reduxjs/toolkit';
22
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
33
import { forEach } from 'lodash-es';
4-
import { Graph } from 'services/api';
4+
import { Graph } from 'services/api/types';
55

66
export const actionSanitizer = <A extends AnyAction>(action: A): A => {
77
if (isAnyGraphBuilt(action)) {

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { startAppListening } from '..';
22
import { log } from 'app/logging/useLogger';
33
import { commitStagingAreaImage } from 'features/canvas/store/canvasSlice';
4-
import { sessionCanceled } from 'services/thunks/session';
4+
import { sessionCanceled } from 'services/api/thunks/session';
55

66
const moduleLog = log.child({ namespace: 'canvas' });
77

@@ -10,7 +10,7 @@ export const addCommitStagingAreaImageListener = () => {
1010
actionCreator: commitStagingAreaImage,
1111
effect: async (action, { dispatch, getState }) => {
1212
const state = getState();
13-
const { sessionId, isProcessing } = state.system;
13+
const { sessionId: session_id, isProcessing } = state.system;
1414
const canvasSessionId = action.payload;
1515

1616
if (!isProcessing) {
@@ -23,20 +23,20 @@ export const addCommitStagingAreaImageListener = () => {
2323
return;
2424
}
2525

26-
if (canvasSessionId !== sessionId) {
26+
if (canvasSessionId !== session_id) {
2727
moduleLog.debug(
2828
{
2929
data: {
3030
canvasSessionId,
31-
sessionId,
31+
session_id,
3232
},
3333
},
3434
'Canvas session does not match global session, skipping cancel'
3535
);
3636
return;
3737
}
3838

39-
dispatch(sessionCanceled({ sessionId }));
39+
dispatch(sessionCanceled({ session_id }));
4040
},
4141
});
4242
};

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@ import { log } from 'app/logging/useLogger';
22
import { startAppListening } from '..';
33
import { boardIdSelected } from 'features/gallery/store/boardSlice';
44
import { selectImagesAll } from 'features/gallery/store/imagesSlice';
5-
import { IMAGES_PER_PAGE, receivedPageOfImages } from 'services/thunks/image';
6-
import { api } from 'services/apiSlice';
5+
import {
6+
IMAGES_PER_PAGE,
7+
receivedPageOfImages,
8+
} from 'services/api/thunks/image';
79
import { imageSelected } from 'features/gallery/store/gallerySlice';
10+
import { boardsApi } from 'services/api/endpoints/boards';
811

912
const moduleLog = log.child({ namespace: 'boards' });
1013

1114
export const addBoardIdSelectedListener = () => {
1215
startAppListening({
1316
actionCreator: boardIdSelected,
1417
effect: (action, { getState, dispatch }) => {
15-
const boardId = action.payload;
18+
const board_id = action.payload;
1619

1720
// we need to check if we need to fetch more images
1821

1922
const state = getState();
2023
const allImages = selectImagesAll(state);
2124

22-
if (!boardId) {
25+
if (!board_id) {
2326
// a board was unselected
2427
dispatch(imageSelected(allImages[0]?.image_name));
2528
return;
@@ -29,13 +32,14 @@ export const addBoardIdSelectedListener = () => {
2932

3033
const filteredImages = allImages.filter((i) => {
3134
const isInCategory = categories.includes(i.image_category);
32-
const isInSelectedBoard = boardId ? i.board_id === boardId : true;
35+
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
3336
return isInCategory && isInSelectedBoard;
3437
});
3538

3639
// get the board from the cache
37-
const { data: boards } = api.endpoints.listAllBoards.select()(state);
38-
const board = boards?.find((b) => b.board_id === boardId);
40+
const { data: boards } =
41+
boardsApi.endpoints.listAllBoards.select()(state);
42+
const board = boards?.find((b) => b.board_id === board_id);
3943

4044
if (!board) {
4145
// can't find the board in cache...
@@ -50,7 +54,7 @@ export const addBoardIdSelectedListener = () => {
5054
filteredImages.length < board.image_count &&
5155
filteredImages.length < IMAGES_PER_PAGE
5256
) {
53-
dispatch(receivedPageOfImages({ categories, boardId }));
57+
dispatch(receivedPageOfImages({ categories, board_id }));
5458
}
5559
},
5660
});
@@ -60,13 +64,13 @@ export const addBoardIdSelected_changeSelectedImage_listener = () => {
6064
startAppListening({
6165
actionCreator: boardIdSelected,
6266
effect: (action, { getState, dispatch }) => {
63-
const boardId = action.payload;
67+
const board_id = action.payload;
6468

6569
const state = getState();
6670

6771
// we need to check if we need to fetch more images
6872

69-
if (!boardId) {
73+
if (!board_id) {
7074
// a board was unselected - we don't need to do anything
7175
return;
7276
}
@@ -75,13 +79,14 @@ export const addBoardIdSelected_changeSelectedImage_listener = () => {
7579

7680
const filteredImages = selectImagesAll(state).filter((i) => {
7781
const isInCategory = categories.includes(i.image_category);
78-
const isInSelectedBoard = boardId ? i.board_id === boardId : true;
82+
const isInSelectedBoard = board_id ? i.board_id === board_id : true;
7983
return isInCategory && isInSelectedBoard;
8084
});
8185

8286
// get the board from the cache
83-
const { data: boards } = api.endpoints.listAllBoards.select()(state);
84-
const board = boards?.find((b) => b.board_id === boardId);
87+
const { data: boards } =
88+
boardsApi.endpoints.listAllBoards.select()(state);
89+
const board = boards?.find((b) => b.board_id === board_id);
8590
if (!board) {
8691
// can't find the board in cache...
8792
return;
@@ -92,7 +97,7 @@ export const addBoardIdSelected_changeSelectedImage_listener = () => {
9297
filteredImages.length < board.image_count &&
9398
filteredImages.length < IMAGES_PER_PAGE
9499
) {
95-
dispatch(receivedPageOfImages({ categories, boardId }));
100+
dispatch(receivedPageOfImages({ categories, board_id }));
96101
}
97102
},
98103
});

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { canvasMerged } from 'features/canvas/store/actions';
22
import { startAppListening } from '..';
33
import { log } from 'app/logging/useLogger';
44
import { addToast } from 'features/system/store/systemSlice';
5-
import { imageUploaded } from 'services/thunks/image';
5+
import { imageUploaded } from 'services/api/thunks/image';
66
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
77
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
88
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
@@ -47,13 +47,11 @@ export const addCanvasMergedListener = () => {
4747

4848
const imageUploadedRequest = dispatch(
4949
imageUploaded({
50-
formData: {
51-
file: new File([blob], 'mergedCanvas.png', {
52-
type: 'image/png',
53-
}),
54-
},
55-
imageCategory: 'general',
56-
isIntermediate: true,
50+
file: new File([blob], 'mergedCanvas.png', {
51+
type: 'image/png',
52+
}),
53+
image_category: 'general',
54+
is_intermediate: true,
5755
postUploadAction: {
5856
type: 'TOAST_CANVAS_MERGED',
5957
},
@@ -68,13 +66,13 @@ export const addCanvasMergedListener = () => {
6866
uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
6967
);
7068

71-
const mergedCanvasImage = payload;
69+
const { image_name } = payload;
7270

7371
dispatch(
7472
setMergedCanvas({
7573
kind: 'image',
7674
layer: 'base',
77-
image: mergedCanvasImage,
75+
imageName: image_name,
7876
...baseLayerRect,
7977
})
8078
);

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { canvasSavedToGallery } from 'features/canvas/store/actions';
22
import { startAppListening } from '..';
33
import { log } from 'app/logging/useLogger';
4-
import { imageUploaded } from 'services/thunks/image';
4+
import { imageUploaded } from 'services/api/thunks/image';
55
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
66
import { addToast } from 'features/system/store/systemSlice';
77
import { imageUpserted } from 'features/gallery/store/imagesSlice';
@@ -30,13 +30,11 @@ export const addCanvasSavedToGalleryListener = () => {
3030

3131
const imageUploadedRequest = dispatch(
3232
imageUploaded({
33-
formData: {
34-
file: new File([blob], 'savedCanvas.png', {
35-
type: 'image/png',
36-
}),
37-
},
38-
imageCategory: 'general',
39-
isIntermediate: false,
33+
file: new File([blob], 'savedCanvas.png', {
34+
type: 'image/png',
35+
}),
36+
image_category: 'general',
37+
is_intermediate: false,
4038
postUploadAction: {
4139
type: 'TOAST_CANVAS_SAVED_TO_GALLERY',
4240
},

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { startAppListening } from '..';
2-
import { imageMetadataReceived } from 'services/thunks/image';
2+
import { imageMetadataReceived } from 'services/api/thunks/image';
33
import { log } from 'app/logging/useLogger';
44
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
5-
import { Graph } from 'services/api';
6-
import { sessionCreated } from 'services/thunks/session';
5+
import { Graph } from 'services/api/types';
6+
import { sessionCreated } from 'services/api/thunks/session';
77
import { sessionReadyToInvoke } from 'features/system/store/actions';
88
import { socketInvocationComplete } from 'services/events/actions';
9-
import { isImageOutput } from 'services/types/guards';
9+
import { isImageOutput } from 'services/api/guards';
1010
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
11-
import { pick } from 'lodash-es';
1211

1312
const moduleLog = log.child({ namespace: 'controlNet' });
1413

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { log } from 'app/logging/useLogger';
22
import { startAppListening } from '..';
3-
import { imageMetadataReceived } from 'services/thunks/image';
4-
import { api } from 'services/apiSlice';
3+
import { imageMetadataReceived } from 'services/api/thunks/image';
4+
import { boardImagesApi } from 'services/api/endpoints/boardImages';
55

66
const moduleLog = log.child({ namespace: 'boards' });
77

88
export const addImageAddedToBoardFulfilledListener = () => {
99
startAppListening({
10-
matcher: api.endpoints.addImageToBoard.matchFulfilled,
10+
matcher: boardImagesApi.endpoints.addImageToBoard.matchFulfilled,
1111
effect: (action, { getState, dispatch }) => {
1212
const { board_id, image_name } = action.meta.arg.originalArgs;
1313

@@ -18,7 +18,7 @@ export const addImageAddedToBoardFulfilledListener = () => {
1818

1919
dispatch(
2020
imageMetadataReceived({
21-
imageName: image_name,
21+
image_name,
2222
})
2323
);
2424
},
@@ -27,7 +27,7 @@ export const addImageAddedToBoardFulfilledListener = () => {
2727

2828
export const addImageAddedToBoardRejectedListener = () => {
2929
startAppListening({
30-
matcher: api.endpoints.addImageToBoard.matchRejected,
30+
matcher: boardImagesApi.endpoints.addImageToBoard.matchRejected,
3131
effect: (action, { getState, dispatch }) => {
3232
const { board_id, image_name } = action.meta.arg.originalArgs;
3333

0 commit comments

Comments
 (0)