Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invokeai/frontend/web/src/app/logging/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const zLogNamespace = z.enum([
'queue',
'workflows',
'video',
'enqueue',
]);
export type LogNamespace = z.infer<typeof zLogNamespace>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,11 @@ const PublishWorkflowButton = memo(() => {
),
duration: null,
});
assert(result.value.enqueueResult.batch.batch_id);
assert(result.value.batchConfig.validation_run_data);
assert(result.value?.enqueueResult.batch.batch_id);
assert(result.value?.batchConfig.validation_run_data);
$validationRunData.set({
batchId: result.value.enqueueResult.batch.batch_id,
workflowId: result.value.batchConfig.validation_run_data.workflow_id,
batchId: result.value?.enqueueResult.batch.batch_id,
workflowId: result.value?.batchConfig.validation_run_data.workflow_id,
});
log.debug(parseify(result.value), 'Enqueued batch');
}
Expand Down
51 changes: 51 additions & 0 deletions invokeai/frontend/web/src/features/queue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Queue Enqueue Patterns

This directory contains the hooks and utilities that translate UI actions into queue batches. The flow is intentionally
modular so adding a new enqueue type (e.g. a new generation mode) follows a predictable recipe.

## Key building blocks

- `hooks/useEnqueue*.ts` – Feature-specific hooks (generate, canvas, upscaling, video, workflows). Each hook wires local
state to the shared enqueue utilities.
- `hooks/utils/graphBuilders.ts` – Maps base models (sdxl, flux, etc.) to their graph builder functions and normalizes
synchronous vs. asynchronous builders.
- `hooks/utils/executeEnqueue.ts` – Orchestrates the enqueue lifecycle:
1. dispatch the `enqueueRequested*` action
2. build the graph/batch data
3. call `queueApi.endpoints.enqueueBatch`
4. run success/error callbacks

## Adding a new enqueue type

1. **Implement the graph builder (if needed).**
- Create the graph construction logic in `features/nodes/util/graph/generation/...` so it returns a
`GraphBuilderReturn`.
- If the builder reuses existing primitives, consider wiring it into `graphBuilders.ts` by extending the `graphBuilderMap`.

2. **Create the enqueue hook.**
- Add `useEnqueue<Feature>.ts` mirroring the existing hooks. Import `executeEnqueue` and supply feature-specific
`build`, `prepareBatch`, and `onSuccess` callbacks.
- If the feature depends on a new base model, add it to `graphBuilders.ts`.

3. **Register the tab in `useInvoke`.**
- `useInvoke.ts` looks up handlers based on the active tab. Import your new hook and call it inside the `switch`
(or future registry) so the UI can enqueue from the feature.

4. **Add Redux action (optional).**
- Most enqueue hooks dispatch a `enqueueRequested*` action for devtools visibility. Create one with `createAction` if
you want similar tracing.

5. **Cover with tests.**
- Unit-test feature-specific behavior (graph selection, batch tweaks). The shared helpers already have coverage in
`hooks/utils/`.

## Tips

- Keep `build` lean: fetch state, compose graph/batch data, and return `null` when prerequisites are missing. The shared
helper will skip enqueueing and your `onError` will handle logging.
- Use the shared `prepareLinearUIBatch` for single-graph UI workflows. For advanced cases (multi-run batches, workflow
validation runs), supply a custom `prepareBatch` function.
- Prefer updating `graphBuilders.ts` when adding a new base model so every image-based enqueue automatically benefits.

With this structure, the main task when introducing a new enqueue type is describing how to build its graph and how to
massage the batch payload—everything else (dispatching, API calls, history updates) is handled by the utilities.
200 changes: 80 additions & 120 deletions invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
Original file line number Diff line number Diff line change
@@ -1,154 +1,114 @@
import type { AlertStatus } from '@invoke-ai/ui-library';
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStore } from 'app/store/store';
import { useAppStore } from 'app/store/storeHooks';
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
import { buildFluxKontextGraph } from 'features/nodes/util/graph/generation/buildFluxKontextGraph';
import { buildGemini2_5Graph } from 'features/nodes/util/graph/generation/buildGemini2_5Graph';
import { buildImagen3Graph } from 'features/nodes/util/graph/generation/buildImagen3Graph';
import { buildImagen4Graph } from 'features/nodes/util/graph/generation/buildImagen4Graph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
import { selectCanvasDestination } from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderArg } from 'features/nodes/util/graph/types';
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import { assert, AssertionError } from 'tsafe';
import { AssertionError } from 'tsafe';

import type { EnqueueBatchArg } from './utils/executeEnqueue';
import { executeEnqueue } from './utils/executeEnqueue';
import { buildGraphForBase } from './utils/graphBuilders';

const log = logger('generation');
export const enqueueRequestedCanvas = createAction('app/enqueueRequestedCanvas');

const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prepend: boolean) => {
const { dispatch, getState } = store;

dispatch(enqueueRequestedCanvas());

const state = getState();

const destination = selectCanvasDestination(state);

const model = state.params.model;
if (!model) {
log.error('No model found in state');
return;
}

const base = model.base;

const buildGraphResult = await withResultAsync(async () => {
const generationMode = await canvasManager.compositor.getGenerationMode();
const graphBuilderArg: GraphBuilderArg = { generationMode, state, manager: canvasManager };

switch (base) {
case 'sdxl':
return await buildSDXLGraph(graphBuilderArg);
case 'sd-1':
case `sd-2`:
return await buildSD1Graph(graphBuilderArg);
case `sd-3`:
return await buildSD3Graph(graphBuilderArg);
case `flux`:
return await buildFLUXGraph(graphBuilderArg);
case 'cogview4':
return await buildCogView4Graph(graphBuilderArg);
case 'imagen3':
return buildImagen3Graph(graphBuilderArg);
case 'imagen4':
return buildImagen4Graph(graphBuilderArg);
case 'chatgpt-4o':
return await buildChatGPT4oGraph(graphBuilderArg);
case 'flux-kontext':
return buildFluxKontextGraph(graphBuilderArg);
case 'gemini-2.5':
return buildGemini2_5Graph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
});

if (buildGraphResult.isErr()) {
let title = 'Failed to build graph';
let status: AlertStatus = 'error';
let description: string | null = null;
if (buildGraphResult.error instanceof AssertionError) {
description = extractMessageFromAssertionError(buildGraphResult.error);
} else if (buildGraphResult.error instanceof UnsupportedGenerationModeError) {
title = 'Unsupported generation mode';
description = buildGraphResult.error.message;
status = 'warning';
}
const error = serializeError(buildGraphResult.error);
log.error({ error }, 'Failed to build graph');
toast({
status,
title,
description,
});
return;
}

const { g, seed, positivePrompt } = buildGraphResult.value;

const prepareBatchResult = withResult(() =>
prepareLinearUIBatch({
state,
g,
base,
prepend,
seedNode: seed,
positivePromptNode: positivePrompt,
origin: 'canvas',
destination,
})
);

if (prepareBatchResult.isErr()) {
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
return;
}

const batchConfig = prepareBatchResult.value;

const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
...enqueueMutationFixedCacheKeyOptions,
track: false,
})
);

const enqueueResult = await req.unwrap();

// Push to prompt history on successful enqueue
dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));

return { batchConfig, enqueueResult };
type CanvasBuildResult = {
batchConfig: EnqueueBatchArg;
};

export const useEnqueueCanvas = () => {
const store = useAppStore();
const canvasManager = useCanvasManagerSafe();

const enqueue = useCallback(
(prepend: boolean) => {
if (!canvasManager) {
log.error('Canvas manager is not available');
return;
return null;
}
return enqueueCanvas(store, canvasManager, prepend);

return executeEnqueue({
store,
options: { prepend },
requestedAction: enqueueRequestedCanvas,
log,
build: async ({ store: innerStore, options }) => {
const state = innerStore.getState();

const destination = selectCanvasDestination(state);
const model = state.params.model;
if (!model) {
log.error('No model found in state');
return null;
}

const generationMode = await canvasManager.compositor.getGenerationMode();
const graphBuilderArg: GraphBuilderArg = { generationMode, state, manager: canvasManager };

const buildGraphResult = await withResultAsync(
async () => await buildGraphForBase(model.base, graphBuilderArg)
);

if (buildGraphResult.isErr()) {
let title = 'Failed to build graph';
let status: AlertStatus = 'error';
let description: string | null = null;
if (buildGraphResult.error instanceof AssertionError) {
description = extractMessageFromAssertionError(buildGraphResult.error);
} else if (buildGraphResult.error instanceof UnsupportedGenerationModeError) {
title = 'Unsupported generation mode';
description = buildGraphResult.error.message;
status = 'warning';
}
const error = serializeError(buildGraphResult.error);
log.error({ error }, 'Failed to build graph');
toast({ status, title, description });
return null;
}

const { g, seed, positivePrompt } = buildGraphResult.value;

const prepareBatchResult = withResult(() =>
prepareLinearUIBatch({
state,
g,
base: model.base,
prepend: options.prepend,
seedNode: seed,
positivePromptNode: positivePrompt,
origin: 'canvas',
destination,
})
);

if (prepareBatchResult.isErr()) {
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
return null;
}

return {
batchConfig: prepareBatchResult.value,
} satisfies CanvasBuildResult;
},
prepareBatch: ({ buildResult }) => buildResult.batchConfig,
onSuccess: ({ store: innerStore }) => {
const state = innerStore.getState();
innerStore.dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));
},
});
},
[canvasManager, store]
);

return enqueue;
};
Loading
Loading