Portal Community

Middleware Structure

// historyMiddleware.ts
import { StateCreator, StoreMutatorIdentifier } from 'zustand';

type HistorySnapshot = {
    nodes : WorkflowNode[];
    edges : WorkflowEdge[];
};

// The middleware adds three fields to the store:
type HistoryExtension = {
    undoStack   : HistorySnapshot[];
    redoStack   : HistorySnapshot[];
    historyAction: (fn: () => void) => void;
};

export const historyMiddleware = <T extends WorkflowState>(
    config: StateCreator<T & HistoryExtension>
): StateCreator<T & HistoryExtension> =>
(set, get, api) => ({
    ...config(set, get, api),
    undoStack   : [],
    redoStack   : [],

    historyAction: (fn: () => void) => {
        const { nodes, edges, undoStack } = get();

        // Take snapshot of graph before mutation
        const snapshot: HistorySnapshot = {
            nodes: JSON.parse(JSON.stringify(nodes)),
            edges: JSON.parse(JSON.stringify(edges))
        };

        // Push to undo stack (cap at HISTORY_STACK_SIZE)
        const newUndoStack = [snapshot, ...undoStack].slice(0, HISTORY_STACK_SIZE);

        // Apply mutation and clear redo stack
        set({ undoStack: newUndoStack, redoStack: [] } as any);
        fn();
    }
});

How workflowStore Uses It

// workflowStore.ts — tracked actions call historyAction
export const useWorkflowStore = create<WorkflowState & HistoryExtension>()(
    historyMiddleware(
        (set, get) => ({
            nodes: [], edges: [],

            addNode: (node) => {
                get().historyAction(() => {
                    set(state => ({ nodes: [...state.nodes, node] }));
                });
            },

            removeNode: (id) => {
                get().historyAction(() => {
                    set(state => ({
                        nodes: state.nodes.filter(n => n.id !== id),
                        edges: state.edges.filter(e => e.source !== id && e.target !== id)
                    }));
                });
            },
            // ...
        })
    )
);

Middleware vs. Separate Store

History is a middleware (not a separate store) because it needs to intercept mutations at the moment they happen — before the state changes. A separate store would receive notifications after the fact and would miss the pre-mutation snapshot.