diff --git a/vehicle-data-platform/apps/web/src/v2/AppV2.cache.test.tsx b/vehicle-data-platform/apps/web/src/v2/AppV2.cache.test.tsx new file mode 100644 index 00000000..7b8367c4 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/AppV2.cache.test.tsx @@ -0,0 +1,26 @@ +import { QueryClientProvider, useMutation } from '@tanstack/react-query'; +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, expect, test } from 'vitest'; +import { createPlatformQueryClient } from './AppV2'; + +afterEach(cleanup); + +test('keeps mutation feedback while mounted and releases completed writes after route unmount', async () => { + const client = createPlatformQueryClient(); + const retainedPayload = { rows: Array.from({ length: 500 }, (_, index) => ({ vin: `VIN-${index}`, model: `MODEL-${index}` })) }; + + function MutationHarness() { + const mutation = useMutation({ mutationFn: async () => retainedPayload }); + return
{mutation.data ? 已处理 {mutation.data.rows.length} 行 : null}
; + } + + const view = render(); + fireEvent.click(screen.getByRole('button', { name: '执行写操作' })); + + expect(await screen.findByText('已处理 500 行')).toBeInTheDocument(); + expect(client.getMutationCache().getAll()).toHaveLength(1); + + view.unmount(); + await waitFor(() => expect(client.getMutationCache().getAll()).toHaveLength(0)); + client.clear(); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/AppV2.tsx b/vehicle-data-platform/apps/web/src/v2/AppV2.tsx index eb022dcc..799028a4 100644 --- a/vehicle-data-platform/apps/web/src/v2/AppV2.tsx +++ b/vehicle-data-platform/apps/web/src/v2/AppV2.tsx @@ -5,17 +5,26 @@ import { AuthGate } from './auth/AuthGate'; import { RoutePage } from './routing/RouteBoundary'; import { RoutePages } from './routing/routeModules'; import { ROUTER_FUTURE } from './routing/routerConfig'; +import { QUERY_MEMORY } from './queryPolicy'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 15_000, - gcTime: 5 * 60_000, - retry: 1, - refetchOnWindowFocus: false +export function createPlatformQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 15_000, + gcTime: 5 * 60_000, + retry: 1, + refetchOnWindowFocus: false + }, + mutations: { + gcTime: QUERY_MEMORY.highVolumeGcTime, + retry: 0 + } } - } -}); + }); +} + +const queryClient = createPlatformQueryClient(); export function AppV2() { return ( diff --git a/vehicle-data-platform/docs/frontend-production-readiness.md b/vehicle-data-platform/docs/frontend-production-readiness.md index a6495cb0..5ba1dd22 100644 --- a/vehicle-data-platform/docs/frontend-production-readiness.md +++ b/vehicle-data-platform/docs/frontend-production-readiness.md @@ -2,6 +2,20 @@ This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready. +## 2026-07-16: route-scoped mutation memory + +The global QueryClient previously left TanStack Query's mutation garbage-collection policy at its default. Completed writes therefore remained in the Mutation Cache after their page unmounted. Most variables are small, but the vehicle-profile synchronization workflow closes over as many as 500 parsed CSV records and performs a dry run followed by an apply mutation. Repeated administration sessions could retain several completed result graphs and their closures for minutes after leaving the page. + +Completed mutations now use zero inactive retention globally. Pending state, success/error feedback and follow-up invalidation remain available for as long as the owning component is mounted; when the route or panel unmounts, the unused mutation record and any referenced batch payload become immediately eligible for garbage collection. Queries keep their separate reuse policy because read caching improves navigation and is already bounded by data cardinality. + +TanStack Query exposes separate Query and Mutation caches and documents `gcTime` as the lifecycle control for unused cache entries. Chrome's memory guidance likewise identifies lingering JavaScript references as the reason removed application objects remain reachable: + +- +- +- + +A component lifecycle test returns a 500-row mutation result, proves the mounted UI can still read it, then unmounts the route owner and proves the Mutation Cache falls from one entry to zero. + ## 2026-07-16: scope-safe mileage queries Mileage Statistics previously reused every successful summary and daily-mileage response as placeholder data for the next query key. After applying another vehicle, date range or source strategy, the new controls and column dates appeared immediately while the summary cards and cells could still contain values from the preceding scope. That visual mismatch could make an operator attribute mileage to the wrong period.