Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type FlowEvent =

export interface Ctx {
item: UpdateQueueItem | null;
queue: UpdateQueueItem[];
}

export interface CreateUpdateQueueMachineOptions {
Expand Down Expand Up @@ -81,6 +82,13 @@ export function createUpdateQueueMachine({
setItem: assign(({ event }) => ({
item: (event as Start).item,
})),
enqueueItem: assign(({ context, event }) => ({
queue: [...context.queue, (event as Start).item],
})),
processNextInQueue: assign(({ context }) => ({
item: context.queue[0] || null,
queue: context.queue.slice(1),
})),
setScopeInstance: assign(({ context }) => ({
item: context.item
? { ...context.item, scope: "instance" as const }
Expand All @@ -102,7 +110,15 @@ export function createUpdateQueueMachine({
}
removeOptimisticAction(context.item.optimisticId);
},
clear: assign(() => ({ item: null })),
clearQueueOptimisticActions: ({ context }) => {
// Clean up optimistic actions for queued items that won't be processed
context.queue.forEach((item) => {
if (item.optimisticId) {
removeOptimisticAction(item.optimisticId);
}
});
},
clear: assign(() => ({ item: null, queue: [] })),
},
actors: {
updateEventActor: fromPromise(
Expand All @@ -111,7 +127,7 @@ export function createUpdateQueueMachine({
},
}).createMachine({
id: "updateEvent",
context: { item: null },
context: { item: null, queue: [] },
initial: "idle",
states: {
idle: {
Expand All @@ -124,7 +140,7 @@ export function createUpdateQueueMachine({
always: [
{
guard: ({ context }) => !context.item?.event,
target: "finalize",
target: "checkQueue",
},
{ guard: "promptRecurringScope", target: "askRecurringScope" },
{ guard: "needsNotify", target: "askNotifyAttendee" },
Expand All @@ -137,32 +153,44 @@ export function createUpdateQueueMachine({
SCOPE_INSTANCE: { target: "route", actions: "setScopeInstance" },
SCOPE_SERIES: { target: "route", actions: "setScopeSeries" },
CANCEL: { target: "rollback" },
START: { actions: "enqueueItem" },
},
},

askNotifyAttendee: {
on: {
NOTIFY_CHOICE: { target: "route", actions: "setNotify" },
CANCEL: { target: "rollback" },
START: { actions: "enqueueItem" },
},
},

mutate: {
on: {
START: { actions: "enqueueItem" },
},
invoke: {
src: "updateEventActor",
input: ({ context }: { context: Ctx }) => context.item!,
onDone: { target: "finalize" },
onDone: { target: "checkQueue" },
onError: { target: "rollback" },
},
},

rollback: {
entry: "removeOptimisticAction",
always: { target: "idle", actions: "clear" },
always: { target: "checkQueue", actions: "clear" },
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 9, 2025

Choose a reason for hiding this comment

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

The rollback state clears the entire update queue (actions: "clear") if a single update fails, but it does not clean up the optimistic UI changes for the discarded queued items. The clearQueueOptimisticActions function was added to handle this cleanup, but it is never called, leading to a leaked UI state where the application shows updates that have been silently dropped.

Prompt for AI agents
Address the following comment on apps/web/src/components/calendar/flows/update-event/update-queue.ts at line 182:

<comment>The `rollback` state clears the entire update queue (`actions: &quot;clear&quot;`) if a single update fails, but it does not clean up the optimistic UI changes for the discarded queued items. The `clearQueueOptimisticActions` function was added to handle this cleanup, but it is never called, leading to a leaked UI state where the application shows updates that have been silently dropped.</comment>

<file context>
@@ -137,32 +153,44 @@ export function createUpdateQueueMachine({
       rollback: {
         entry: &quot;removeOptimisticAction&quot;,
-        always: { target: &quot;idle&quot;, actions: &quot;clear&quot; },
+        always: { target: &quot;checkQueue&quot;, actions: &quot;clear&quot; },
       },
 
</file context>
Fix with Cubic

},

finalize: {
always: { target: "idle", actions: "clear" },
checkQueue: {
always: [
{
guard: ({ context }) => context.queue.length > 0,
target: "route",
actions: "processNextInQueue",
},
{ target: "idle" },
],
},
},
});
Expand Down