Syncing...
@@ -54,7 +54,7 @@ export function SyncStatusBadge({
Synced
@@ -63,7 +63,7 @@ export function SyncStatusBadge({
{formatRelativeTime(lastSyncedAt)}
@@ -74,7 +74,7 @@ export function SyncStatusBadge({
Disconnected
@@ -84,7 +84,7 @@ export function SyncStatusBadge({
Waiting for first sync
diff --git a/apps/web/hooks/use-trigger-sync.ts b/apps/web/hooks/use-trigger-sync.ts
index f7f701e9..5df0b8f0 100644
--- a/apps/web/hooks/use-trigger-sync.ts
+++ b/apps/web/hooks/use-trigger-sync.ts
@@ -3,7 +3,12 @@
import { $fetch } from "@lib/api"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
+import type { ConnectionResponseSchema } from "@repo/validation/api"
+import type { z } from "zod"
import type { ImportProvider } from "@/components/settings/sync-utils"
+import type { SyncRun } from "@/hooks/use-sync-runs"
+
+type Connection = z.infer
export function useTriggerSync() {
const queryClient = useQueryClient()
@@ -13,9 +18,7 @@ export function useTriggerSync() {
provider,
containerTags,
}: {
- // connectionId is not sent to the backend — the import endpoint is keyed
- // by provider, so it re-syncs all connections for that provider.
- // It's kept here so onSuccess can target cache invalidation.
+ // connectionId isn't sent to the backend (import is keyed by provider) — kept for cache updates
connectionId: string
provider: ImportProvider
containerTags?: string[]
@@ -32,15 +35,60 @@ export function useTriggerSync() {
}
return response.data
},
- onSuccess: (_data, variables) => {
+ // Optimistically flip to "syncing" so the badge/button update instantly; the 5s poll then converges on real state
+ onMutate: async (variables) => {
+ await queryClient.cancelQueries({ queryKey: ["connections"] })
+ const previousConnections = queryClient.getQueryData([
+ "connections",
+ ])
+ queryClient.setQueryData(["connections"], (old) =>
+ old?.map((c) =>
+ c.provider === variables.provider
+ ? {
+ ...c,
+ metadata: {
+ ...((c.metadata as Record | null) ?? {}),
+ syncInProgress: true,
+ },
+ }
+ : c,
+ ),
+ )
+
+ const syncRunsKey = ["sync-runs", variables.connectionId]
+ const previousSyncRuns = queryClient.getQueryData(syncRunsKey)
+ if (previousSyncRuns) {
+ const optimisticRun: SyncRun = {
+ id: `optimistic-${Date.now()}`,
+ connectionId: variables.connectionId,
+ status: "running",
+ triggerType: "manual",
+ startedAt: new Date().toISOString(),
+ completedAt: null,
+ itemsProcessed: 0,
+ itemsFailed: 0,
+ error: null,
+ }
+ queryClient.setQueryData(syncRunsKey, [
+ optimisticRun,
+ ...previousSyncRuns,
+ ])
+ }
+
+ return { previousConnections, previousSyncRuns, syncRunsKey }
+ },
+ // Don't invalidate connections/sync-runs here — an immediate refetch races the backend and clobbers the optimistic state; the 5s polls handle it
+ onSuccess: () => {
toast.success("Sync started")
- queryClient.invalidateQueries({ queryKey: ["connections"] })
- queryClient.invalidateQueries({
- queryKey: ["sync-runs", variables.connectionId],
- })
queryClient.invalidateQueries({ queryKey: ["processing-documents"] })
},
- onError: (error) => {
+ onError: (error, _variables, context) => {
+ if (context?.previousConnections !== undefined) {
+ queryClient.setQueryData(["connections"], context.previousConnections)
+ }
+ if (context?.previousSyncRuns !== undefined) {
+ queryClient.setQueryData(context.syncRunsKey, context.previousSyncRuns)
+ }
toast.error("Failed to start sync", {
description: error instanceof Error ? error.message : "Unknown error",
})