Files
multica/packages/core/workspace/mutations.ts
Multica Eve ac62f72c2a MUL-4480: make daemon workspace sync event-driven (#5354)
* feat(daemon): make workspace sync event-driven

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): preserve trailing workspace changes

Co-authored-by: multica-agent <github@multica.ai>

* fix(workspace): reconcile failed creates

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-14 11:56:30 +08:00

85 lines
3.8 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]);
},
// A failed create should not have changed server state, but invalidate the
// list once to reconcile ambiguous transport failures where the server may
// have committed before the client lost the response. Keep this off the
// success path so the canonical response seeded above is not fetched again.
onError: () => {
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() });
},
});
}