Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,31 @@ export function GenerateContentRunner({
const upsertMessage = useGenerationStore((s) => s.upsertMessage);
const updateGeneration = useGenerationStore((s) => s.updateGeneration);
const { updateGenerationStatusToComplete } = useGenerationRunnerSystem();
const didRun = useRef(false);
const didPerformingContentGeneration = useRef(false);
const didListeningContentGeneration = useRef(false);
const reachedStreamEnd = useRef(false);
const messageUpdateQueue = useRef<Map<UIMessage["id"], UIMessage>>(new Map());
const pendingUpdate = useRef<number | null>(null);
const prevGenerationId = useRef(generation.id);

// Reset lifecycle refs when generation changes
useEffect(() => {
if (prevGenerationId.current === generation.id) {
return;
}
didPerformingContentGeneration.current = false;
didListeningContentGeneration.current = false;
reachedStreamEnd.current = false;

// Clear message queue to prevent stale messages from previous generation
messageUpdateQueue.current.clear();

// Cancel pending animation frame to prevent applying stale updates
if (pendingUpdate.current !== null) {
cancelAnimationFrame(pendingUpdate.current);
pendingUpdate.current = null;
}
}, [generation.id]);

const flushMessageUpdates = useCallback(() => {
if (messageUpdateQueue.current.size === 0) return;
Expand Down Expand Up @@ -160,20 +181,32 @@ export function GenerateContentRunner({
]);

useEffect(() => {
if (didRun.current) {
if (didPerformingContentGeneration.current) {
return;
}
if (generation.status !== "queued") {
return;
}
didRun.current = true;
didPerformingContentGeneration.current = true;
client
.startContentGeneration({ generation })
.then(({ generation: runningGeneration }) => {
onStart?.(runningGeneration);
updateGeneration(runningGeneration);
processStream();
});
}, [generation, client, processStream, updateGeneration, onStart]);
}, [generation, client, updateGeneration, onStart]);

useEffect(() => {
if (didListeningContentGeneration.current) {
return;
}
if (generation.status !== "running") {
return;
}
didListeningContentGeneration.current = true;

processStream();
}, [generation, processStream]);

return null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NodeId } from "@giselle-sdk/data-type";
import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import useSWR from "swr";
import { useShallow } from "zustand/shallow";
import type {
Expand Down Expand Up @@ -30,12 +30,13 @@ export function useNodeGenerations({
startGenerationRunner,
createAndStartGenerationRunner,
stopGenerationRunner: stopGenerationSystem,
addGenerationRunner,
} = useGenerationRunnerSystem();
const client = useGiselleEngine();
const { experimental_storage } = useFeatureFlag();

/** @todo fetch on server */
const { data } = useSWR(
const { data, isLoading } = useSWR(
{
api: "node-generations",
origin,
Expand All @@ -49,35 +50,33 @@ export function useNodeGenerations({
revalidateOnReconnect: false,
},
);

useEffect(() => {
if (isLoading || data === undefined) {
return;
}
addGenerationRunner(data);
}, [isLoading, data, addGenerationRunner]);

const currentGeneration = useMemo<Generation>(() => {
const fetchGenerations = data ?? [];
const createdGenerations = generations.filter(
(generation) =>
generation.context.operationNode.id === nodeId &&
generation.context.origin.type === origin.type &&
(origin.type === "studio"
? generation.context.origin.type === "studio" &&
generation.context.origin.workspaceId === origin.workspaceId
: generation.context.origin.type !== "studio" &&
generation.context.origin.actId === origin.actId),
);
// Deduplicate generations by filtering out fetched generations from created ones
const deduplicatedCreatedGenerations = createdGenerations.filter(
(created) =>
!fetchGenerations.some((fetched) => fetched.id === created.id),
);
// Filter out cancelled generations from both sources after deduplication
const allGenerations = [
...fetchGenerations,
...deduplicatedCreatedGenerations,
]
.filter((generation) => generation.status !== "cancelled")
const filteredGenerations = generations
.filter(
(generation) =>
generation.status !== "cancelled" &&
generation.context.operationNode.id === nodeId &&
generation.context.origin.type === origin.type &&
(origin.type === "studio"
? generation.context.origin.type === "studio" &&
generation.context.origin.workspaceId === origin.workspaceId
: generation.context.origin.type !== "studio" &&
generation.context.origin.actId === origin.actId),
)
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
return allGenerations[0];
}, [generations, data, nodeId, origin]);
return filteredGenerations[0];
}, [generations, nodeId, origin]);
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency array is missing data which is used indirectly through the syncing effect. Consider adding data to ensure the memo recalculates when remote data changes, or document why it's intentionally excluded.

Suggested change
}, [generations, nodeId, origin]);
}, [generations, nodeId, origin, data]);

Copilot uses AI. Check for mistakes.


const isGenerating = useMemo(
() =>
Expand Down
6 changes: 5 additions & 1 deletion packages/giselle/src/react/generations/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ export const useGenerationStore = create<GenerationStore>((set) => ({
addGenerationRunner: (generations) =>
set((state) => {
const arr = Array.isArray(generations) ? generations : [generations];
const incomingIds = new Set(arr.map((g) => g.id));
const filteredExisting = state.generations.filter(
(g) => !incomingIds.has(g.id),
);
return {
generations: [...state.generations, ...arr],
generations: [...filteredExisting, ...arr],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Local Generation Data Overwritten by Server

The addGenerationRunner function's deduplication logic replaces existing generations with incoming ones if their IDs match. This can cause data loss for locally running generations, as real-time messages accumulated via upsertMessage are lost when server-fetched data replaces the local generation in the store.

Additional Locations (1)

Fix in Cursor Fix in Web

};
}),
updateGeneration: (generation) =>
Expand Down