feat(ui): optimized workflow building

- Store workflow in nanostore as singleton instead of building for each consumer
- Debounce the build (already was indirectly debounced)
- When the workflow is needed, imperatively grab it from the nanostores, instead of letting react handle it via reactivity
This commit is contained in:
psychedelicious 2024-01-01 15:54:31 +11:00 committed by Kent Keirsey
parent 7eb79266c4
commit 7e2eeec1f3
8 changed files with 67 additions and 47 deletions

View File

@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger'; import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import { import {
connectionEnded, connectionEnded,
connectionMade, connectionMade,
@ -80,7 +81,7 @@ export const Flow = memo(() => {
const flowWrapper = useRef<HTMLDivElement>(null); const flowWrapper = useRef<HTMLDivElement>(null);
const cursorPosition = useRef<XYPosition | null>(null); const cursorPosition = useRef<XYPosition | null>(null);
const isValidConnection = useIsValidConnection(); const isValidConnection = useIsValidConnection();
useWorkflowWatcher();
const [borderRadius] = useToken('radii', ['base']); const [borderRadius] = useToken('radii', ['base']);
const flowStyles = useMemo<CSSProperties>( const flowStyles = useMemo<CSSProperties>(

View File

@ -1,16 +1,17 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const WorkflowJSONTab = () => { const WorkflowJSONTab = () => {
const workflow = useWorkflow(); const workflow = useStore($builtWorkflow);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Flex flexDir="column" alignItems="flex-start" gap={2} h="full"> <Flex flexDir="column" alignItems="flex-start" gap={2} h="full">
<DataViewer data={workflow} label={t('nodes.workflow')} /> <DataViewer data={workflow ?? {}} label={t('nodes.workflow')} />
</Flex> </Flex>
); );
}; };

View File

@ -1,19 +0,0 @@
import { useAppSelector } from 'app/store/storeHooks';
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
import { useMemo } from 'react';
import { useDebounce } from 'use-debounce';
export const useWorkflow = () => {
const nodes_ = useAppSelector((state) => state.nodes.nodes);
const edges_ = useAppSelector((state) => state.nodes.edges);
const workflow_ = useAppSelector((state) => state.workflow);
const [nodes] = useDebounce(nodes_, 300);
const [edges] = useDebounce(edges_, 300);
const [workflow] = useDebounce(workflow_, 300);
const builtWorkflow = useMemo(
() => buildWorkflow({ nodes, edges, workflow }),
[nodes, edges, workflow]
);
return builtWorkflow;
};

View File

@ -0,0 +1,25 @@
import { useAppSelector } from 'app/store/storeHooks';
import type { WorkflowV2 } from 'features/nodes/types/workflow';
import type { BuildWorkflowArg } from 'features/nodes/util/workflow/buildWorkflow';
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
import { debounce } from 'lodash-es';
import { atom } from 'nanostores';
import { useEffect } from 'react';
export const $builtWorkflow = atom<WorkflowV2 | null>(null);
const debouncedBuildWorkflow = debounce((arg: BuildWorkflowArg) => {
$builtWorkflow.set(buildWorkflow(arg));
}, 300);
export const useWorkflowWatcher = () => {
const buildWorkflowArg = useAppSelector(({ nodes, workflow }) => ({
nodes: nodes.nodes,
edges: nodes.edges,
workflow,
}));
useEffect(() => {
debouncedBuildWorkflow(buildWorkflowArg);
}, [buildWorkflowArg]);
};

View File

@ -2,19 +2,23 @@ import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize'; import { parseify } from 'common/util/serialize';
import type { NodesState, WorkflowsState } from 'features/nodes/store/types'; import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import type { WorkflowV2 } from 'features/nodes/types/workflow'; import type {
import { zWorkflowEdge, zWorkflowNode } from 'features/nodes/types/workflow'; WorkflowV2} from 'features/nodes/types/workflow';
import i18n from 'i18next'; import {
zWorkflowEdge,
zWorkflowNode,
} from 'features/nodes/types/workflow';
import i18n from 'i18n';
import { cloneDeep, omit } from 'lodash-es'; import { cloneDeep, omit } from 'lodash-es';
import { fromZodError } from 'zod-validation-error'; import { fromZodError } from 'zod-validation-error';
type BuildWorkflowArg = { export type BuildWorkflowArg = {
nodes: NodesState['nodes']; nodes: NodesState['nodes'];
edges: NodesState['edges']; edges: NodesState['edges'];
workflow: WorkflowsState; workflow: WorkflowsState;
}; };
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2; export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2;
export const buildWorkflow: BuildWorkflowFunction = ({ export const buildWorkflow: BuildWorkflowFunction = ({
nodes, nodes,

View File

@ -1,17 +1,19 @@
import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
import { useCallback } from 'react';
const downloadWorkflow = () => {
const workflow = $builtWorkflow.get();
if (!workflow) {
return;
}
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${workflow.name || 'My Workflow'}.json`;
document.body.appendChild(a);
a.click();
a.remove();
};
export const useDownloadWorkflow = () => { export const useDownloadWorkflow = () => {
const workflow = useWorkflow();
const downloadWorkflow = useCallback(() => {
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${workflow.name || 'My Workflow'}.json`;
document.body.appendChild(a);
a.click();
a.remove();
}, [workflow]);
return downloadWorkflow; return downloadWorkflow;
}; };

View File

@ -1,7 +1,7 @@
import type { ToastId } from '@chakra-ui/react'; import type { ToastId } from '@chakra-ui/react';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
import { import {
workflowIDChanged, workflowIDChanged,
workflowSaved, workflowSaved,
@ -30,12 +30,15 @@ const isWorkflowWithID = (
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const workflow = useWorkflow();
const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation(); const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation();
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
const toast = useToast(); const toast = useToast();
const toastRef = useRef<ToastId | undefined>(); const toastRef = useRef<ToastId | undefined>();
const saveWorkflow = useCallback(async () => { const saveWorkflow = useCallback(async () => {
const workflow = $builtWorkflow.get();
if (!workflow) {
return;
}
toastRef.current = toast({ toastRef.current = toast({
title: t('workflows.savingWorkflow'), title: t('workflows.savingWorkflow'),
status: 'loading', status: 'loading',
@ -64,7 +67,7 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
isClosable: true, isClosable: true,
}); });
} }
}, [workflow, updateWorkflow, dispatch, toast, t, createWorkflow]); }, [updateWorkflow, dispatch, toast, t, createWorkflow]);
return { return {
saveWorkflow, saveWorkflow,
isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading, isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading,

View File

@ -1,7 +1,7 @@
import type { ToastId } from '@chakra-ui/react'; import type { ToastId } from '@chakra-ui/react';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
import { import {
workflowIDChanged, workflowIDChanged,
workflowNameChanged, workflowNameChanged,
@ -28,12 +28,15 @@ type UseSaveWorkflowAs = () => UseSaveWorkflowAsReturn;
export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const workflow = useWorkflow();
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
const toast = useToast(); const toast = useToast();
const toastRef = useRef<ToastId | undefined>(); const toastRef = useRef<ToastId | undefined>();
const saveWorkflowAs = useCallback( const saveWorkflowAs = useCallback(
async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => {
const workflow = $builtWorkflow.get();
if (!workflow) {
return;
}
toastRef.current = toast({ toastRef.current = toast({
title: t('workflows.savingWorkflow'), title: t('workflows.savingWorkflow'),
status: 'loading', status: 'loading',
@ -64,7 +67,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
}); });
} }
}, },
[toast, workflow, createWorkflow, dispatch, t] [toast, createWorkflow, dispatch, t]
); );
return { return {
saveWorkflowAs, saveWorkflowAs,