API Data Fetching and Caching
Initial Page Load
-
App initialization (
src/App.tsx:39)QueryClientProviderwraps the app with TanStack Query- React Router mounts the active page component
-
Component mounts and triggers query
- Example: Dashboard calls
useTasks()hook - Hook location:
src/api/hooks.ts:33
- Example: Dashboard calls
-
Query execution
-
GraphQL fetch (
src/api/graphql.ts:3)POST https://api.tarkov.dev/graphql- Error handling for network/GraphQL errors
-
Normalization
- Raw API response → flattened application models
- Example:
normalizeTasks()builds dependency graph and extracts objective maps
-
Memory cache
- TanStack Query stores result keyed by
['tasks', lang] - Data is marked fresh for 12 hours
gcTime: Infinityprevents eviction
- TanStack Query stores result keyed by
Subsequent Renders
- Same component calls
useTasks()again - TanStack Query returns cached data instantly (no re-fetch)
- If data is stale (>12h), Query refetches in background and updates UI on completion
Cache Invalidation
Manual refresh (Settings page):useTasks() call.
API data is never invalidated automatically. Users must manually refresh cache if they want to pull latest upstream changes (e.g., new quests after a game patch).
User Action → State Update → Persistence Flow
Example: Completing a Task
- User clicks “Mark as Done” in task detail panel
-
Component calls domain function
-
Dexie write operation
-
IndexedDB transaction
- Dexie executes atomic write
- Returns immediately (synchronous from caller perspective)
-
UI update
- Component re-renders with new data from
useProgress()hook - Task badge changes to “Done”
- Kappa progress bar updates
- Recommendation scores recalculate (dependent tasks may now unlock)
- Component re-renders with new data from
Example: Adding a Map Pin
- User clicks map to place pin
- Map component calls:
- Dexie writes to
mapPinstable - UI shows new pin immediately (optimistic update via React state)
Lock/Unlock Calculation Flow
Task lock state is never stored — it’s computed on every render.Flow
- Component needs lock state (e.g., TaskCard rendering)
-
Fetch dependencies
-
Build progress map
-
Call unlock function (
src/domain/unlock.ts:11) -
Unlock logic executes
- Check
profile.level >= task.minPlayerLevel - Check all
task.prereqIdsare"done"inprogressMap - Return
trueonly if both conditions pass
- Check
- UI renders lock badge/icon based on result
Why Dynamic Calculation?
- Simplicity: No cache invalidation needed
- Correctness: Always reflects current player level + progress
- Performance: Unlock check is O(n) where n = # of prerequisites (typically 0-3)
For ~200 quests, full unlock state calculation takes less than 1ms. The simplicity tradeoff is worth more than premature caching optimization.
Recommendation Scoring Pipeline
The Dashboard “Recommended Tasks” panel runs a multi-stage scoring pipeline.Pipeline Steps
-
Filter to candidates (
src/domain/recommendations.ts:105) -
Build reverse dependency graph
- Input:
prereqEdges(taskId → prerequisite IDs[]) - Output:
reverseDeps(taskId → dependent task IDs[])
- Input:
-
For each candidate, compute scores:
a. Downstream impact (
domain/recommendations.ts:49)- BFS traversal from task through reverse deps
- Counts only incomplete Kappa-required tasks
- Example: Completing “Debut” unlocks 15 downstream Kappa tasks → score 15
- Counts other unlocked Kappa tasks on the same map
- Example: If 4 other Customs tasks are available → score 4
-
Normalize and weight
- Sort by composite score descending
- Return top 5 tasks
Example Calculation
| Task | Downstream | Map Batch | Kappa? | Normalized Score | Composite |
|---|---|---|---|---|---|
| Debut | 15 | 3 | Yes | (1.0 × 0.5) + (0.6 × 0.3) + (1.0 × 0.2) | 0.88 |
| Cargo X Pt.2 | 0 | 5 | Yes | (0 × 0.5) + (1.0 × 0.3) + (1.0 × 0.2) | 0.50 |
| Fishing Gear | 8 | 1 | No | (0.53 × 0.5) + (0.2 × 0.3) + (0 × 0.2) | 0.33 |
Real-Time vs Cached Data Decisions
Always Real-Time (Never Cached)
- Task unlock state: Depends on current level + progress
- Kappa progress %: Aggregates live progress data
- Recommendation scores: Requires live dependency graph
- Hideout buildable state: Depends on item inventory counts
Cached (Memory)
- Quest metadata: Names, objectives, prerequisites (from API)
- Map images: URLs to 2D/3D map images
- Item prices: Flea market price per slot (from API)
- Trader data: Trader names, images
Persisted (IndexedDB)
- Task completion status: User progress
- Player level: User profile
- Map pins: User-placed pins with labels
- Notes: Per-task text notes
- Hideout progress: Construction state
- Item inventory: Owned item counts
Error Handling Flows
Network Error During Initial Load
- Query enters loading state
- After 2 retries (configurable), Query enters error state
- Component shows error UI: “Failed to fetch tasks. Check connection.”
- User can click “Retry” → triggers
queryClient.refetchQueries()
Network Error After Cache Populated
- Query attempts background refetch
- Fetch fails after retries
- Query keeps existing cached data and marks as stale
- User sees last successful data (potentially 12h+ old)
- Small “offline” indicator shows in UI
IndexedDB Write Failure
- Dexie throws error (e.g., storage quota exceeded)
- Component catches error and shows toast notification
- Data is not optimistically updated (no partial state)
- User can free space and retry action
IndexedDB failures are rare but catastrophic. The app shows a persistent warning banner if IndexedDB is unavailable (e.g., private browsing mode in some browsers).
Performance Optimizations
Query Key Granularity
- Prevents unnecessary re-fetches when language toggle occurs
- English and Japanese task names coexist in cache
Dexie Compound Indexes
- Fast lookup: “Get all pins for Customs + wipe123 + 2D view”
- Avoids full table scan on map render
Memoized Domain Functions
Components useuseMemo to avoid re-running expensive calculations: