Skip to content

Commit 3c902ae

Browse files
Wirasmclaude
andcommitted
refactor: complete Phase 2 Query Keys Standardization
Standardize query keys across all features following vertical slice architecture, ensuring they mirror backend API structure exactly with no backward compatibility. Key Changes: - Refactor all query key factories to follow consistent patterns - Move progress feature from knowledge/progress to top-level /features/progress - Create shared query patterns for consistency (DISABLED_QUERY_KEY, STALE_TIMES) - Remove all hardcoded stale times and disabled keys - Update all imports after progress feature relocation Query Key Factories Standardized: - projectKeys: removed task-related keys (tasks, taskCounts) - taskKeys: added dual nature support (global via lists(), project-scoped via byProject()) - knowledgeKeys: removed redundant methods (details, summary) - progressKeys: new top-level feature with consistent factory - documentKeys: full factory pattern with versions support - mcpKeys: complete with health endpoint Shared Patterns Implementation: - STALE_TIMES: instant (0), realtime (3s), frequent (5s), normal (30s), rare (5m), static (∞) - DISABLED_QUERY_KEY: consistent disabled query pattern across all features - Removed unused createQueryOptions helper Testing: - Added comprehensive tests for progress hooks - Updated all test mocks to include new STALE_TIMES values - All 81 feature tests passing Documentation: - Created QUERY_PATTERNS.md guide for future implementations - Clear patterns, examples, and migration checklist Breaking Changes: - Progress imports moved from knowledge/progress to progress - Query key structure changes (cache will reset) - No backward compatibility maintained Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b383c8c commit 3c902ae

31 files changed

Lines changed: 663 additions & 152 deletions

PRPs/ai_docs/QUERY_PATTERNS.md

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# TanStack Query Patterns Guide
2+
3+
This guide documents the standardized patterns for using TanStack Query v5 in the Archon frontend.
4+
5+
## Core Principles
6+
7+
1. **Feature Ownership**: Each feature owns its query keys in `{feature}/hooks/use{Feature}Queries.ts`
8+
2. **Consistent Patterns**: Always use shared patterns from `shared/queryPatterns.ts`
9+
3. **No Hardcoded Values**: Never hardcode stale times or disabled keys
10+
4. **Mirror Backend API**: Query keys should exactly match backend API structure
11+
12+
## Query Key Factory Pattern
13+
14+
Every feature MUST implement a query key factory following this pattern:
15+
16+
```typescript
17+
// features/{feature}/hooks/use{Feature}Queries.ts
18+
export const featureKeys = {
19+
all: ["feature"] as const, // Base key for the domain
20+
lists: () => [...featureKeys.all, "list"] as const, // For list endpoints
21+
detail: (id: string) => [...featureKeys.all, "detail", id] as const, // For single item
22+
// Add more as needed following backend routes
23+
};
24+
```
25+
26+
### Examples from Codebase
27+
28+
```typescript
29+
// Projects - Simple hierarchy
30+
export const projectKeys = {
31+
all: ["projects"] as const,
32+
lists: () => [...projectKeys.all, "list"] as const,
33+
detail: (id: string) => [...projectKeys.all, "detail", id] as const,
34+
features: (id: string) => [...projectKeys.all, id, "features"] as const,
35+
};
36+
37+
// Tasks - Dual nature (global and project-scoped)
38+
export const taskKeys = {
39+
all: ["tasks"] as const,
40+
lists: () => [...taskKeys.all, "list"] as const, // /api/tasks
41+
detail: (id: string) => [...taskKeys.all, "detail", id] as const,
42+
byProject: (projectId: string) => ["projects", projectId, "tasks"] as const, // /api/projects/{id}/tasks
43+
counts: () => [...taskKeys.all, "counts"] as const,
44+
};
45+
```
46+
47+
## Shared Patterns Usage
48+
49+
### Import Required Patterns
50+
51+
```typescript
52+
import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/queryPatterns";
53+
```
54+
55+
### Disabled Queries
56+
57+
Always use `DISABLED_QUERY_KEY` when a query should not execute:
58+
59+
```typescript
60+
// ✅ CORRECT
61+
queryKey: projectId ? projectKeys.detail(projectId) : DISABLED_QUERY_KEY,
62+
63+
// ❌ WRONG - Don't create custom disabled keys
64+
queryKey: projectId ? projectKeys.detail(projectId) : ["projects-undefined"],
65+
```
66+
67+
### Stale Times
68+
69+
Always use `STALE_TIMES` constants for cache configuration:
70+
71+
```typescript
72+
// ✅ CORRECT
73+
staleTime: STALE_TIMES.normal, // 30 seconds
74+
staleTime: STALE_TIMES.frequent, // 5 seconds
75+
staleTime: STALE_TIMES.instant, // 0 - always fresh
76+
77+
// ❌ WRONG - Don't hardcode times
78+
staleTime: 30000,
79+
staleTime: 0,
80+
```
81+
82+
#### STALE_TIMES Reference
83+
84+
- `instant: 0` - Always fresh (real-time data like active progress)
85+
- `realtime: 3_000` - 3 seconds (near real-time updates)
86+
- `frequent: 5_000` - 5 seconds (frequently changing data)
87+
- `normal: 30_000` - 30 seconds (standard cache time)
88+
- `rare: 300_000` - 5 minutes (rarely changing config)
89+
- `static: Infinity` - Never stale (settings, auth)
90+
91+
## Complete Hook Pattern
92+
93+
```typescript
94+
export function useFeatureDetail(id: string | undefined) {
95+
return useQuery({
96+
queryKey: id ? featureKeys.detail(id) : DISABLED_QUERY_KEY,
97+
queryFn: () => id
98+
? featureService.getFeatureById(id)
99+
: Promise.reject("No ID provided"),
100+
enabled: !!id,
101+
staleTime: STALE_TIMES.normal,
102+
});
103+
}
104+
```
105+
106+
## Mutations with Optimistic Updates
107+
108+
```typescript
109+
export function useCreateFeature() {
110+
const queryClient = useQueryClient();
111+
112+
return useMutation({
113+
mutationFn: (data: CreateFeatureRequest) => featureService.create(data),
114+
115+
onMutate: async (newData) => {
116+
// Cancel in-flight queries
117+
await queryClient.cancelQueries({ queryKey: featureKeys.lists() });
118+
119+
// Snapshot for rollback
120+
const previous = queryClient.getQueryData(featureKeys.lists());
121+
122+
// Optimistic update (use timestamp IDs for now - Phase 3 will use UUIDs)
123+
const tempId = `temp-${Date.now()}`;
124+
queryClient.setQueryData(featureKeys.lists(), (old: Feature[] = []) =>
125+
[...old, { ...newData, id: tempId }]
126+
);
127+
128+
return { previous, tempId };
129+
},
130+
131+
onError: (err, variables, context) => {
132+
// Rollback on error
133+
if (context?.previous) {
134+
queryClient.setQueryData(featureKeys.lists(), context.previous);
135+
}
136+
},
137+
138+
onSuccess: (data, variables, context) => {
139+
// Replace optimistic with real data
140+
queryClient.setQueryData(featureKeys.lists(), (old: Feature[] = []) =>
141+
old.map(item => item.id === context?.tempId ? data : item)
142+
);
143+
},
144+
});
145+
}
146+
```
147+
148+
## Testing Query Hooks
149+
150+
Always mock both services and shared patterns:
151+
152+
```typescript
153+
// Mock services
154+
vi.mock("../../services", () => ({
155+
featureService: {
156+
getList: vi.fn(),
157+
getById: vi.fn(),
158+
},
159+
}));
160+
161+
// Mock shared patterns with ALL values
162+
vi.mock("../../../shared/queryPatterns", () => ({
163+
DISABLED_QUERY_KEY: ["disabled"] as const,
164+
STALE_TIMES: {
165+
instant: 0,
166+
realtime: 3_000,
167+
frequent: 5_000,
168+
normal: 30_000,
169+
rare: 300_000,
170+
static: Infinity,
171+
},
172+
}));
173+
```
174+
175+
## Vertical Slice Architecture
176+
177+
Each feature is self-contained:
178+
179+
```
180+
src/features/projects/
181+
├── components/ # UI components
182+
├── hooks/
183+
│ └── useProjectQueries.ts # Query hooks & keys
184+
├── services/
185+
│ └── projectService.ts # API calls
186+
└── types/
187+
└── index.ts # TypeScript types
188+
```
189+
190+
Sub-features (like tasks under projects) follow the same structure:
191+
192+
```
193+
src/features/projects/tasks/
194+
├── components/
195+
├── hooks/
196+
│ └── useTaskQueries.ts # Own query keys!
197+
├── services/
198+
└── types/
199+
```
200+
201+
## Migration Checklist
202+
203+
When refactoring to these patterns:
204+
205+
- [ ] Create query key factory in `hooks/use{Feature}Queries.ts`
206+
- [ ] Import `DISABLED_QUERY_KEY` and `STALE_TIMES` from shared
207+
- [ ] Replace all hardcoded disabled keys with `DISABLED_QUERY_KEY`
208+
- [ ] Replace all hardcoded stale times with `STALE_TIMES` constants
209+
- [ ] Update all `queryKey` references to use factory
210+
- [ ] Update all `invalidateQueries` to use factory
211+
- [ ] Update all `setQueryData` to use factory
212+
- [ ] Add comprehensive tests for query keys
213+
- [ ] Remove any backward compatibility code
214+
215+
## Common Pitfalls to Avoid
216+
217+
1. **Don't create centralized query keys** - Each feature owns its keys
218+
2. **Don't hardcode values** - Use shared constants
219+
3. **Don't mix concerns** - Tasks shouldn't import projectKeys
220+
4. **Don't skip mocking in tests** - Mock both services and patterns
221+
5. **Don't use inconsistent patterns** - Follow the established conventions
222+
223+
## Future Improvements (Phase 3+)
224+
225+
- Replace timestamp IDs (`temp-${Date.now()}`) with UUIDs
226+
- Add Server-Sent Events for real-time updates
227+
- Consider Zustand for complex client state

archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useToast } from "../../ui/hooks/useToast";
99
import { Button, Input, Label } from "../../ui/primitives";
1010
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/primitives/dialog";
1111
import { cn } from "../../ui/primitives/styles";
12-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives/tabs";
12+
import { Tabs, TabsContent } from "../../ui/primitives/tabs";
1313
import { useCrawlUrl, useUploadDocument } from "../hooks";
1414
import type { CrawlRequest, UploadMetadata } from "../types";
1515
import { KnowledgeTypeSelector } from "./KnowledgeTypeSelector";

archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { format } from "date-fns";
88
import { motion } from "framer-motion";
99
import { Briefcase, Clock, Code, ExternalLink, File, FileText, Globe, Terminal } from "lucide-react";
1010
import { useState } from "react";
11+
import { KnowledgeCardProgress } from "../../progress/components/KnowledgeCardProgress";
12+
import type { ActiveOperation } from "../../progress/types";
1113
import { StatPill } from "../../ui/primitives";
1214
import { cn } from "../../ui/primitives/styles";
1315
import { SimpleTooltip } from "../../ui/primitives/tooltip";
1416
import { useDeleteKnowledgeItem, useRefreshKnowledgeItem } from "../hooks";
15-
import { KnowledgeCardProgress } from "../progress/components/KnowledgeCardProgress";
16-
import type { ActiveOperation } from "../progress/types";
1717
import type { KnowledgeItem } from "../types";
1818
import { extractDomain } from "../utils/knowledge-utils";
1919
import { KnowledgeCardActions } from "./KnowledgeCardActions";
@@ -232,7 +232,7 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
232232
<KnowledgeCardTitle
233233
sourceId={item.source_id}
234234
title={item.title}
235-
description={(item as any).summary}
235+
description={item.metadata?.description}
236236
accentColor={getAccentColorName()}
237237
/>
238238
</div>

archon-ui-main/src/features/knowledge/components/KnowledgeList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { AnimatePresence, motion } from "framer-motion";
77
import { AlertCircle, Loader2 } from "lucide-react";
88
import { Button } from "../../ui/primitives";
9-
import type { ActiveOperation } from "../progress/types";
9+
import type { ActiveOperation } from "../../progress/types";
1010
import type { KnowledgeItem } from "../types";
1111
import { KnowledgeCard } from "./KnowledgeCard";
1212
import { KnowledgeTable } from "./KnowledgeTable";

archon-ui-main/src/features/knowledge/hooks/tests/useKnowledgeQueries.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,18 @@ describe("useKnowledgeQueries", () => {
6262
expect(knowledgeKeys.detail("source-123")).toEqual(["knowledge", "detail", "source-123"]);
6363
expect(knowledgeKeys.chunks("source-123", "example.com")).toEqual([
6464
"knowledge",
65-
"detail",
6665
"source-123",
6766
"chunks",
6867
"example.com",
6968
]);
70-
expect(knowledgeKeys.codeExamples("source-123")).toEqual(["knowledge", "detail", "source-123", "code-examples"]);
69+
expect(knowledgeKeys.codeExamples("source-123")).toEqual(["knowledge", "source-123", "code-examples"]);
7170
expect(knowledgeKeys.search("test query")).toEqual(["knowledge", "search", "test query"]);
7271
expect(knowledgeKeys.sources()).toEqual(["knowledge", "sources"]);
7372
});
7473

75-
it("should handle filter in list key", () => {
74+
it("should handle filter in summaries key", () => {
7675
const filter = { knowledge_type: "technical" as const, page: 2 };
77-
expect(knowledgeKeys.list(filter)).toEqual(["knowledge", "list", filter]);
76+
expect(knowledgeKeys.summaries(filter)).toEqual(["knowledge", "summaries", filter]);
7877
});
7978
});
8079

@@ -127,7 +126,7 @@ describe("useKnowledgeQueries", () => {
127126

128127
// Pre-populate cache
129128
const queryClient = new QueryClient();
130-
queryClient.setQueryData(knowledgeKeys.list(), initialData);
129+
queryClient.setQueryData(knowledgeKeys.lists(), initialData);
131130

132131
await result.current.mutateAsync("source-1");
133132

0 commit comments

Comments
 (0)