mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 18:13:27 +02:00
* fix(workspace): drop workspace from list cache while delete is pending (MUL-4129) The delete-workspace flow navigates away before awaiting the DELETE (required ordering — see navigateAwayFromCurrentWorkspace's CancelledError notes), but useDeleteWorkspace left the workspace in the list cache until onSettled. During the pending window any list refetch re-presented the deleting workspace as a selectable/current option. Optimistically remove it in onMutate (after cancelling in-flight list fetches), roll the snapshot back in onError so a failed delete restores the workspace alongside the existing error toast, and keep the onSettled invalidate as the server-truth reconcile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(workspace): own storage cleanup on delete success and tombstone pending deletes (MUL-4129) Address final-review blockers on #4980: 1. The realtime workspace:deleted handler reverse-looks-up the slug from the list cache to clear the ${key}:${slug} persisted namespace; the optimistic removal empties that row on the initiating client, so the lookup misses and cleanup was silently skipped. Capture the slug in onMutate before removal and clear storage in onSuccess only — a failed DELETE rolls back and must not touch persisted state. 2. cancelQueries only covered fetches already in flight at onMutate. Add a pending-delete tombstone (marked onMutate, lifted onSettled before the reconcile invalidate) filtered inside workspaceListOptions' queryFn, so invalidation/reconnect/fetchQuery refetches that land mid-pending cannot write the not-yet-committed row back into cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs(claude-md): scope optimistic updates to same-screen field patches Replace the blanket "mutations optimistic by default" state rule with three scoped rules: optimistic only for predictable same-screen field patches; navigating/confirming flows (create/delete/leave) await the server first; chat send uses the pending-message pattern. Aligned with TanStack Query maintainer guidance and React Router's pending-UI criteria; the old blanket rule is what steered the original MUL-4129 fix toward optimistic entity removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workspace): await delete before navigating; drop optimistic removal (MUL-4129) Rework of the previous approach on this branch. The optimistic removal emptied the workspace list cache at click time, while the settings page was still mounted on the old slug — useWorkspaceId (URL slug + list lookup) then threw 'no workspace selected'. Root cause of the original navigate-first ordering was dual ownership of delete handling between the initiating flow and the realtime workspace:deleted handler. - useDeleteWorkspace: no optimistic removal, no rollback; onMutate only marks the delete self-initiated and captures the slug; onSuccess owns storage cleanup; onSettled invalidates. - pending-delete.ts: repurposed from tombstone filter to self-initiated marker; kept on success (suppresses the WS echo), lifted on failure. - use-realtime-sync: workspace:deleted no-ops for self-initiated deletes; it now only serves deletes initiated elsewhere. - workspace-tab: confirm dialog stays open in loading state, navigate only after the DELETE succeeds; failure leaves the user in place with nothing to roll back. Replace throwing useWorkspaceId with workspace?.id + enabled gating (independent crash on external deletes of the current workspace). Known debt: useLeaveWorkspace still navigates before awaiting (member:removed has no self-initiated marker yet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
81 lines
3.5 KiB
TypeScript
81 lines
3.5 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import type { Workspace } from "../types";
|
|
import { api } from "../api";
|
|
import { defaultStorage } from "../platform/storage";
|
|
import { clearWorkspaceStorage } from "../platform/storage-cleanup";
|
|
import { workspaceKeys } from "./queries";
|
|
import {
|
|
markWorkspaceDeletePending,
|
|
unmarkWorkspaceDeletePending,
|
|
} from "./pending-delete";
|
|
|
|
export function useCreateWorkspace() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: { name: string; slug: string; description?: string }) =>
|
|
api.createWorkspace(data),
|
|
// Seed the workspace list cache BEFORE callers navigate to /{newWs.slug}/issues.
|
|
// The destination [workspaceSlug]/layout queries by slug from this cache;
|
|
// without seeding, it would briefly show "loading" before the background
|
|
// invalidation completes. TanStack Query guarantees this onSuccess runs
|
|
// before mutateAsync's resolver / before any callback-style onSuccess
|
|
// passed to mutate(), so any caller that navigates after the mutation
|
|
// resolves will see the seeded data synchronously. Switching workspaces
|
|
// is pure navigation now — no imperative store writes needed.
|
|
onSuccess: (newWs) => {
|
|
qc.setQueryData(workspaceKeys.list(), (old: Workspace[] = []) => [...old, newWs]);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useLeaveWorkspace() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (workspaceId: string) => api.leaveWorkspace(workspaceId),
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteWorkspace() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (workspaceId: string) => api.deleteWorkspace(workspaceId),
|
|
// No optimistic removal: the delete flow awaits this mutation with the
|
|
// confirm dialog in a loading state and only navigates on success, so
|
|
// the cache staying truthful (server still has the row until commit)
|
|
// is correct, and a failed DELETE needs no rollback.
|
|
onMutate: (workspaceId) => {
|
|
// Mark the delete as self-initiated so the realtime `workspace:deleted`
|
|
// handler no-ops instead of racing this flow's navigation with its own
|
|
// full-page relocate. See pending-delete.ts for lifetime rules.
|
|
markWorkspaceDeletePending(workspaceId);
|
|
// Capture the slug for onSuccess's storage cleanup — cheap here, and
|
|
// the row is guaranteed to still be in the list pre-mutation.
|
|
const slug = qc
|
|
.getQueryData<Workspace[]>(workspaceKeys.list())
|
|
?.find((w) => w.id === workspaceId)?.slug;
|
|
return { slug };
|
|
},
|
|
// Success is the only path that clears the deleted workspace's persisted
|
|
// `${key}:${slug}` namespace — a failed DELETE means the workspace still
|
|
// exists and its drafts/view state must survive. The realtime handler
|
|
// skips self-initiated deletes, so cleanup has to happen here.
|
|
onSuccess: (_data, _workspaceId, ctx) => {
|
|
if (ctx?.slug) clearWorkspaceStorage(defaultStorage, ctx.slug);
|
|
},
|
|
// The workspace still exists after a failed DELETE, so a later external
|
|
// delete of the same ID must be handled by the realtime handler again.
|
|
onError: (_err, workspaceId) => {
|
|
unmarkWorkspaceDeletePending(workspaceId);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
|
|
},
|
|
});
|
|
}
|