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

View File

@ -1,16 +1,17 @@
import { Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
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 { useTranslation } from 'react-i18next';
const WorkflowJSONTab = () => {
const workflow = useWorkflow();
const workflow = useStore($builtWorkflow);
const { t } = useTranslation();
return (
<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>
);
};

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 type { NodesState, WorkflowsState } from 'features/nodes/store/types';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import type { WorkflowV2 } from 'features/nodes/types/workflow';
import { zWorkflowEdge, zWorkflowNode } from 'features/nodes/types/workflow';
import i18n from 'i18next';
import type {
WorkflowV2} from 'features/nodes/types/workflow';
import {
zWorkflowEdge,
zWorkflowNode,
} from 'features/nodes/types/workflow';
import i18n from 'i18n';
import { cloneDeep, omit } from 'lodash-es';
import { fromZodError } from 'zod-validation-error';
type BuildWorkflowArg = {
export type BuildWorkflowArg = {
nodes: NodesState['nodes'];
edges: NodesState['edges'];
workflow: WorkflowsState;
};
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2;
export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2;
export const buildWorkflow: BuildWorkflowFunction = ({
nodes,

View File

@ -1,17 +1,19 @@
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { useCallback } from 'react';
import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
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 = () => {
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;
};

View File

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

View File

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