mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 12:34:50 +02:00
[eric] workflows: settle seq forces a post-commit occurrences refetch, the optimistic toggle can no longer strand Coming up on pre-commit truth
This commit is contained in:
@@ -78,14 +78,16 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
|
||||
.join('|');
|
||||
const fromIso = rangeStart.toISOString();
|
||||
const toIso = rangeEndExclusive.toISOString();
|
||||
const calendarRequestKey = `${view}:${fromIso}:${toIso}:${workflowScheduleKey}`;
|
||||
// Same race as useCalendarOccurrences: the optimistic pending update flips the fingerprint pre-commit; settleSeq forces one post-commit refetch and busts the interceptor's 1s URL cache.
|
||||
const settleSeq = useAppSelector((s) => s.workflows.settleSeq);
|
||||
const calendarRequestKey = `${view}:${fromIso}:${toIso}:${workflowScheduleKey}:${settleSeq}`;
|
||||
// The visible window alone decides whether shown events are even plausible. Gating on this (not the full request key) means a schedule edit refetches without blanking the calendar first: we keep the current events until the fresh ones land. Only a view/date change, where old events are for the wrong window, clears them.
|
||||
const calendarWindowKey = `${view}:${fromIso}:${toIso}`;
|
||||
|
||||
useEffect(() => {
|
||||
// No AbortController: the global fetch interceptor (shared/config) dedupes GETs by URL onto ONE underlying request, so aborting on cleanup (which fires when this effect re-runs as workflows hydrate) rejects the shared request and the re-fired fetch with it, leaving the calendar empty on first load. The `cancelled` guard already stops stale state writes.
|
||||
let cancelled = false;
|
||||
fetch(`${API_BASE}/workflows/calendar?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}`)
|
||||
fetch(`${API_BASE}/workflows/calendar?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}&settle=${settleSeq}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`calendar failed ${res.status}`);
|
||||
return res.json();
|
||||
|
||||
@@ -22,15 +22,17 @@ export function useCalendarOccurrences(fromIso: string, toIso: string): {
|
||||
[items],
|
||||
);
|
||||
|
||||
// The optimistic pending update flips the fingerprint BEFORE the PATCH commits, so the fingerprint-keyed fetch can read pre-commit truth and then never re-ask (the "toggled the schedule off but Coming up still shows it" bug). settleSeq bumps when the write settles, forcing one authoritative refetch; it also rides the URL so the interceptor's 1s GET cache cannot serve the raced response back.
|
||||
const settleSeq = useAppSelector((s) => s.workflows.settleSeq);
|
||||
const windowKey = `${fromIso}:${toIso}`;
|
||||
const requestKey = `${windowKey}:${scheduleKey}`;
|
||||
const requestKey = `${windowKey}:${scheduleKey}:${settleSeq}`;
|
||||
const [events, setEvents] = useState<CalendarOccurrence[]>([]);
|
||||
const [fetchedWindowKey, setFetchedWindowKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// No AbortController: the global fetch interceptor dedupes GETs by URL onto one request, so aborting on re-run (as workflows hydrate) would reject the shared request. The `cancelled` guard stops stale state writes instead.
|
||||
let cancelled = false;
|
||||
fetch(`${API_BASE}/workflows/calendar?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}`)
|
||||
fetch(`${API_BASE}/workflows/calendar?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}&settle=${settleSeq}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`calendar failed ${res.status}`);
|
||||
return res.json();
|
||||
|
||||
@@ -208,9 +208,11 @@ interface State {
|
||||
runControlPending: Record<string, WorkflowRunControlAction>;
|
||||
deleted: Workflow[];
|
||||
deletedLoading: boolean;
|
||||
/** Bumped when a workflow WRITE settles server-side. The optimistic pending update changes the schedule fingerprint BEFORE the PATCH commits, so an occurrences fetch keyed only on the fingerprint reads pre-commit truth and never re-asks; this seq forces one authoritative refetch per settled write. */
|
||||
settleSeq: number;
|
||||
}
|
||||
|
||||
const initialState: State = { items: {}, runs: {}, openCards: {}, loaded: false, loading: false, paused: false, active: [], cloudSmsEnabled: false, allRuns: [], allRunsLoading: false, runningToast: null, noticeToast: null, runControlPending: {}, deleted: [], deletedLoading: false };
|
||||
const initialState: State = { items: {}, runs: {}, openCards: {}, loaded: false, loading: false, paused: false, active: [], cloudSmsEnabled: false, allRuns: [], allRunsLoading: false, runningToast: null, noticeToast: null, runControlPending: {}, deleted: [], deletedLoading: false, settleSeq: 0 };
|
||||
|
||||
function mergeRunIntoState(state: State, r: WorkflowRun) {
|
||||
const arr = state.runs[r.workflow_id] || [];
|
||||
@@ -601,9 +603,9 @@ const slice = createSlice({
|
||||
const cur = state.items[id];
|
||||
if (cur) state.items[id] = { ...cur, ...patch };
|
||||
})
|
||||
.addCase(updateWorkflow.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(commitDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(discardDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(updateWorkflow.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; state.settleSeq += 1; })
|
||||
.addCase(commitDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; state.settleSeq += 1; })
|
||||
.addCase(discardDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; state.settleSeq += 1; })
|
||||
.addCase(deleteWorkflow.rejected, (state, action) => {
|
||||
state.noticeToast = typeof action.payload === 'string' ? action.payload : "Couldn't delete this workflow. Try again in a moment.";
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user