feat(FN-4854): complete Step 3 — hydrate documents and todo list caches

Fusion-Task-Id: FN-4854
Fusion-Task-Lineage: 969ec04a-efe3-4dee-b183-2d4f2de6b388
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 00:27:11 -07:00
committed by gsxdsm
parent 9feb4ec870
commit 8ca0cddfe7
2 changed files with 62 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import type { TaskDocumentWithTask } from "@fusion/core"; import type { TaskDocumentWithTask } from "@fusion/core";
import { fetchAllDocuments, fetchProjectMarkdownFiles, type MarkdownFileEntry } from "../api"; import { fetchAllDocuments, fetchProjectMarkdownFiles, type MarkdownFileEntry } from "../api";
import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache";
export interface UseDocumentsResult { export interface UseDocumentsResult {
/** List of all documents across tasks */ /** List of all documents across tasks */
@@ -32,13 +33,20 @@ export function useDocuments(options?: {
includeProjectFiles?: boolean; includeProjectFiles?: boolean;
}): UseDocumentsResult { }): UseDocumentsResult {
const { projectId, searchQuery, includeProjectFiles = true } = options ?? {}; const { projectId, searchQuery, includeProjectFiles = true } = options ?? {};
const [documents, setDocuments] = useState<TaskDocumentWithTask[]>([]); const cacheKey = projectId ? `${SWR_CACHE_KEYS.DOCUMENTS_PREFIX}${projectId}` : null;
const [documents, setDocuments] = useState<TaskDocumentWithTask[]>(() => {
if (!cacheKey) {
return [];
}
const cached = readCache<TaskDocumentWithTask[]>(cacheKey);
return Array.isArray(cached) ? cached : [];
});
const [projectFiles, setProjectFiles] = useState<MarkdownFileEntry[]>([]); const [projectFiles, setProjectFiles] = useState<MarkdownFileEntry[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(() => documents.length === 0);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
// Track if we've completed the initial load // Track if we've completed the initial load
const initialLoadCompleteRef = useRef(false); const initialLoadCompleteRef = useRef(documents.length > 0);
// Debounce timer for search // Debounce timer for search
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -55,7 +63,7 @@ export function useDocuments(options?: {
const requestController = new AbortController(); const requestController = new AbortController();
abortRef.current = requestController; abortRef.current = requestController;
// Only set loading on initial load // Only set loading on initial load when we have no cached docs.
const isInitial = !initialLoadCompleteRef.current; const isInitial = !initialLoadCompleteRef.current;
if (isInitial) { if (isInitial) {
setLoading(true); setLoading(true);
@@ -84,6 +92,10 @@ export function useDocuments(options?: {
if (documentResult.status === "fulfilled") { if (documentResult.status === "fulfilled") {
setDocuments(documentResult.value); setDocuments(documentResult.value);
if (cacheKey) {
const cachedPayload = documentResult.value.length > 500 ? documentResult.value.slice(0, 500) : documentResult.value;
writeCache(cacheKey, cachedPayload, { maxBytes: 500_000 });
}
initialLoadCompleteRef.current = true; initialLoadCompleteRef.current = true;
} else { } else {
documentError = documentResult.reason instanceof Error documentError = documentResult.reason instanceof Error
@@ -107,7 +119,27 @@ export function useDocuments(options?: {
if (isInitial) { if (isInitial) {
setLoading(false); setLoading(false);
} }
}, [includeProjectFiles, projectId, searchQuery]); }, [cacheKey, includeProjectFiles, projectId, searchQuery]);
useEffect(() => {
if (!cacheKey) {
initialLoadCompleteRef.current = false;
setDocuments([]);
setLoading(true);
return;
}
const cached = readCache<TaskDocumentWithTask[]>(cacheKey);
if (Array.isArray(cached)) {
setDocuments(cached);
initialLoadCompleteRef.current = true;
setLoading(false);
} else {
initialLoadCompleteRef.current = false;
setDocuments([]);
setLoading(true);
}
}, [cacheKey]);
// Debounced search effect // Debounced search effect
useEffect(() => { useEffect(() => {

View File

@@ -10,6 +10,7 @@ import {
deleteTodoItem, deleteTodoItem,
reorderTodoItems, reorderTodoItems,
} from "../api"; } from "../api";
import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache";
type ToastType = "info" | "success" | "error" | "warning"; type ToastType = "info" | "success" | "error" | "warning";
@@ -50,9 +51,13 @@ function buildTempId(prefix: string): string {
export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsResult { export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsResult {
const { projectId, addToast } = options; const { projectId, addToast } = options;
const [lists, setLists] = useState<TodoList[]>([]); const cacheKey = `${SWR_CACHE_KEYS.TODO_LISTS_PREFIX}${projectId ?? "global"}`;
const [lists, setLists] = useState<TodoList[]>(() => {
const cached = readCache<TodoList[]>(cacheKey);
return Array.isArray(cached) ? cached : [];
});
const [items, setItems] = useState<TodoItem[]>([]); const [items, setItems] = useState<TodoItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(() => lists.length === 0);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedListId, setSelectedListId] = useState<string | null>(null); const [selectedListId, setSelectedListId] = useState<string | null>(null);
const [listData, setListData] = useState<TodoListWithItems[]>([]); const [listData, setListData] = useState<TodoListWithItems[]>([]);
@@ -63,8 +68,21 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
async function loadLists() { const cachedLists = readCache<TodoList[]>(cacheKey);
const hasCachedLists = Array.isArray(cachedLists) && cachedLists.length > 0;
if (hasCachedLists) {
setLists(cachedLists);
setLoading(false);
} else {
setLists([]);
setLoading(true); setLoading(true);
}
async function loadLists() {
if (!hasCachedLists) {
setLoading(true);
}
setError(null); setError(null);
try { try {
@@ -74,7 +92,9 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
} }
setListData(data); setListData(data);
setLists(data.map(toList)); const fetchedLists = data.map(toList);
setLists(fetchedLists);
writeCache(cacheKey, fetchedLists, { maxBytes: 500_000 });
const activeListId = const activeListId =
selectedListIdRef.current && data.some((list) => list.id === selectedListIdRef.current) selectedListIdRef.current && data.some((list) => list.id === selectedListIdRef.current)
@@ -104,7 +124,7 @@ export function useTodoLists(options: UseTodoListsOptions = {}): UseTodoListsRes
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [projectId]); }, [cacheKey, projectId]);
useEffect(() => { useEffect(() => {
if (!selectedListId) { if (!selectedListId) {