Skip to content

Commit 45febcb

Browse files
committed
fix(mobile): resolve cache sync issues and modularize notes API
1 parent 68de4b9 commit 45febcb

File tree

9 files changed

+810
-37
lines changed

9 files changed

+810
-37
lines changed

apps/mobile/v1/src/screens/FolderNotes/components/NotesList/NoteListItem.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ const styles = StyleSheet.create({
286286
flexDirection: 'row',
287287
alignItems: 'center',
288288
justifyContent: 'space-between',
289-
marginBottom: 6,
289+
marginBottom: 8,
290290
},
291291
noteListTitle: {
292292
fontSize: 17,
@@ -315,14 +315,14 @@ const styles = StyleSheet.create({
315315
},
316316
noteListPreview: {
317317
fontSize: 15,
318-
lineHeight: 20,
319-
marginBottom: 4,
318+
lineHeight: 22,
319+
marginBottom: 6,
320320
},
321321
noteListFolderInfo: {
322322
flexDirection: 'row',
323323
alignItems: 'center',
324324
gap: 6,
325-
marginTop: 4,
325+
marginTop: 6,
326326
},
327327
noteListFolderDot: {
328328
width: 8,
@@ -338,6 +338,7 @@ const styles = StyleSheet.create({
338338
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
339339
height: StyleSheet.hairlineWidth,
340340
marginHorizontal: 16,
341+
marginVertical: 0,
341342
},
342343
deleteAction: {
343344
backgroundColor: '#ef4444',

apps/mobile/v1/src/screens/FolderNotes/components/NotesList/index.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Ionicons } from '@expo/vector-icons';
21
import { useUser } from '@clerk/clerk-expo';
2+
import { Ionicons } from '@expo/vector-icons';
33
import { BottomSheetModal } from '@gorhom/bottom-sheet';
4-
import { FlashList } from '@shopify/flash-list';
54
import AsyncStorage from '@react-native-async-storage/async-storage';
65
import { useFocusEffect } from '@react-navigation/native';
6+
import { FlashList, type FlashList as FlashListType } from '@shopify/flash-list';
77
import * as Haptics from 'expo-haptics';
88
import { useRouter } from 'expo-router';
99
import React, { useCallback,useEffect, useMemo, useRef, useState } from 'react';
@@ -97,7 +97,7 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
9797
const createFolderSheetRef = useRef<BottomSheetModal>(null);
9898
const filterSortSheetRef = useRef<BottomSheetModal>(null);
9999
const noteActionsSheetRef = useRef<NoteActionsSheetRef>(null);
100-
const flatListRef = useRef<FlashList<Note>>(null);
100+
const flatListRef = useRef<FlashListType<Note>>(null);
101101

102102
// Scroll tracking for animated divider (use parent's scrollY if provided)
103103
const localScrollY = useRef(new Animated.Value(0)).current;
@@ -592,10 +592,9 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
592592
data={filteredNotes}
593593
renderItem={renderNoteItem}
594594
keyExtractor={(item) => item.id}
595-
estimatedItemSize={108}
596595
ListHeaderComponent={renderListHeader}
597596
ListEmptyComponent={renderEmptyComponent}
598-
ListFooterComponent={<View style={{ height: 100 }} />}
597+
ListFooterComponent={<View style={{ height: 120 }} />}
599598
showsVerticalScrollIndicator={false}
600599
onScroll={Animated.event(
601600
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
@@ -616,8 +615,9 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
616615
return 'note';
617616
}}
618617
overrideItemLayout={(layout, item) => {
619-
// Fixed layout for all items
620-
layout.size = 108;
618+
// Fixed layout for better scrolling performance
619+
// Padding: 12*2=24, Header: 23+8, Preview: 40+6, Meta: 20, Divider: 1
620+
layout.size = 112;
621621
}}
622622
/>
623623

apps/mobile/v1/src/screens/FolderNotes/components/NotesList/useNotesLoader.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,16 @@ export function useNotesLoader({
302302
const cacheAge = Date.now() - cacheTimestampRef.current;
303303
const cacheIsFresh = cacheLoadedRef.current && cacheAge < 30000; // 30 seconds
304304

305-
if (cacheIsFresh && cachedNotesRef.current.length > 0) {
305+
// CRITICAL: Verify cache count matches API result count
306+
// If counts differ, fresh data is available (new note created, note deleted, etc.)
307+
// In this case, skip ultra-fast mode and use the fresh API data
308+
const cacheCountMatches = cachedNotesRef.current.length === sortedNotes.length;
309+
310+
if (!cacheCountMatches && cacheIsFresh) {
311+
console.log(`[PERF ⚡⚡⚡] SKIPPING ULTRA FAST MODE: Cache count mismatch (cached: ${cachedNotesRef.current.length}, API: ${sortedNotes.length}) - using fresh data`);
312+
}
313+
314+
if (cacheIsFresh && cachedNotesRef.current.length > 0 && cacheCountMatches) {
306315
console.log(`[PERF ⚡⚡⚡] ULTRA FAST MODE: Cache is ${(cacheAge / 1000).toFixed(1)}s old - SKIPPING ALL DECRYPTION!`);
307316
console.log(`[PERF ⚡⚡⚡] Using ${cachedNotesRef.current.length} cached notes directly - INSTANT LOAD!`);
308317

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Notes API - Modular Structure
2+
3+
The notes API has been modularized to improve maintainability and make it easier to find and fix bugs.
4+
5+
## Structure
6+
7+
```
8+
services/api/notes/
9+
├── index.ts # Main export, combines all modules
10+
├── crud.ts # Create, update, delete operations (~400 lines)
11+
├── core.ts # Query and other operations (to be further extracted)
12+
├── cache-sync.ts # Cache synchronization utilities
13+
├── shared.ts # Shared helper functions
14+
└── README.md # This file
15+
```
16+
17+
## Modules
18+
19+
### `crud.ts` - Create, Update, Delete Operations
20+
**~400 lines** | ✅ Modularized
21+
22+
Contains:
23+
- `createNote()` - Create new notes with offline support
24+
- `updateNote()` - Update existing notes with cache sync
25+
- `deleteNote()` - Delete notes
26+
27+
**Key Features:**
28+
- Automatic cache synchronization (SQLite + AsyncStorage)
29+
- Offline support with optimistic updates
30+
- Proper error handling and logging
31+
32+
### `cache-sync.ts` - Cache Synchronization
33+
**~50 lines** | ✅ Modularized
34+
35+
Contains:
36+
- `updateSQLiteCache()` - Update local database cache
37+
- `clearAsyncStorageCaches()` - Clear preview caches
38+
- `syncAllCaches()` - Sync all cache layers
39+
40+
**Purpose:** Centralized cache management to prevent stale data issues.
41+
42+
### `shared.ts` - Shared Utilities
43+
**~40 lines** | ✅ Modularized
44+
45+
Contains:
46+
- `invalidateCountsCache()` - Invalidate counts and database caches
47+
48+
### `core.ts` - Core Operations
49+
**~1000 lines** | 🔄 To be further modularized
50+
51+
Currently contains:
52+
- Query operations (`getNotes`, `getNote`, `getCounts`)
53+
- Visibility operations (`hideNote`, `unhideNote`)
54+
- Attachment operations (file upload/download)
55+
- Trash operations (`emptyTrash`)
56+
57+
**Next Steps:** Extract these into separate modules:
58+
- `queries.ts` - Query operations
59+
- `visibility.ts` - Hide/unhide operations
60+
- `attachments.ts` - File operations
61+
62+
## Benefits of Modularization
63+
64+
### 1. Easier Bug Fixes
65+
**Before:** Search through 1269 lines to find delete logic
66+
**After:** Go directly to `crud.ts` (~400 lines)
67+
68+
### 2. Better Code Organization
69+
Each module has a single responsibility:
70+
- CRUD operations → `crud.ts`
71+
- Cache management → `cache-sync.ts`
72+
- Shared utilities → `shared.ts`
73+
74+
### 3. Easier Testing
75+
Each module can be tested independently:
76+
```typescript
77+
import { createCrudOperations } from './crud';
78+
// Test just CRUD operations
79+
```
80+
81+
### 4. Cache Issue Resolution
82+
The cache synchronization bugs were caused by mixing cache logic throughout the file. Now it's centralized in `cache-sync.ts`, making it obvious when caches need to sync.
83+
84+
## Cache Architecture
85+
86+
The app has **3 cache layers:**
87+
88+
1. **SQLite Database** (`databaseCache.ts`)
89+
- Persistent local storage
90+
- Survives app restarts
91+
- Updated by: `updateSQLiteCache()`
92+
93+
2. **AsyncStorage** (`useNotesLoader.ts`)
94+
- UI preview cache
95+
- Fast rendering
96+
- Cleared by: `clearAsyncStorageCaches()`
97+
98+
3. **In-Memory Refs** (`useNotesLoader.ts`)
99+
- Ultra-fast mode (< 30 seconds)
100+
- Skip decryption
101+
- Auto-invalidated on count mismatch
102+
103+
### Cache Sync Strategy
104+
105+
**Create/Update Operations:**
106+
```typescript
107+
await syncAllCaches(note, shouldClearAsyncStorage);
108+
// 1. Always update SQLite cache
109+
// 2. Optionally clear AsyncStorage (for delete/archive/move)
110+
```
111+
112+
**When to Clear AsyncStorage:**
113+
- ✅ Note deleted (`deleted: true`)
114+
- ✅ Note archived (`archived: true`)
115+
- ✅ Note moved to different folder (`folderId` changed)
116+
- ❌ Note content edited (keep cache for performance)
117+
118+
## Migration Guide
119+
120+
No changes needed! The modular structure maintains the same API:
121+
122+
```typescript
123+
const api = useApiService();
124+
125+
// All operations work the same
126+
await api.createNote({ title, content });
127+
await api.updateNote(noteId, { deleted: true });
128+
await api.deleteNote(noteId);
129+
```
130+
131+
## Future Enhancements
132+
133+
1. **Extract Remaining Modules:**
134+
- [ ] `queries.ts` - Query operations
135+
- [ ] `visibility.ts` - Hide/unhide operations
136+
- [ ] `attachments.ts` - File operations
137+
138+
2. **Add Unit Tests:**
139+
- [ ] Test CRUD operations independently
140+
- [ ] Test cache synchronization logic
141+
- [ ] Mock network/database for faster tests
142+
143+
3. **Performance Optimizations:**
144+
- [ ] Batch cache updates
145+
- [ ] Optimize background refresh
146+
- [ ] Add request deduplication
147+
148+
## Troubleshooting
149+
150+
### "Deleted notes reappearing"
151+
**Root Cause:** Cache synchronization issue
152+
**Fixed in:** `crud.ts` + `cache-sync.ts`
153+
- `updateNote()` now syncs all caches
154+
- AsyncStorage cleared for location changes
155+
- SQLite always updated
156+
157+
### "New notes not appearing"
158+
**Root Cause:** Ultra-fast mode using stale cache
159+
**Fixed in:** `useNotesLoader.ts`
160+
- Count mismatch detection
161+
- Skip ultra-fast mode if counts differ
162+
163+
### "Notes showing [ENCRYPTED]"
164+
**Root Cause:** Background refresh storing encrypted content
165+
**Fixed in:** `core.ts`
166+
- Always cache decrypted content
167+
- Removed preference check
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Cache synchronization utilities
3+
* Handles keeping SQLite and AsyncStorage caches in sync
4+
*/
5+
6+
import AsyncStorage from '@react-native-async-storage/async-storage';
7+
8+
import type { Note } from '../types';
9+
import { storeCachedNotes } from '../databaseCache';
10+
11+
/**
12+
* Update SQLite cache with a note after create/update
13+
* This ensures the local database cache stays in sync with the server
14+
*/
15+
export async function updateSQLiteCache(note: Note): Promise<void> {
16+
try {
17+
await storeCachedNotes([note], { storeDecrypted: true });
18+
if (__DEV__) {
19+
console.log(`[CacheSync] ✅ Updated SQLite cache for note ${note.id}`);
20+
}
21+
} catch (error) {
22+
console.warn('[CacheSync] Failed to update SQLite cache:', error);
23+
}
24+
}
25+
26+
/**
27+
* Clear AsyncStorage preview caches
28+
* Called when notes change location/status (delete, archive, move folder)
29+
*/
30+
export async function clearAsyncStorageCaches(): Promise<void> {
31+
try {
32+
const allKeys = await AsyncStorage.getAllKeys();
33+
const noteCacheKeys = allKeys.filter(key => key.startsWith('notes-cache-v2-'));
34+
if (noteCacheKeys.length > 0) {
35+
await AsyncStorage.multiRemove(noteCacheKeys);
36+
if (__DEV__) {
37+
console.log(`[CacheSync] ✅ Cleared ${noteCacheKeys.length} AsyncStorage cache(s)`);
38+
}
39+
}
40+
} catch (error) {
41+
console.warn('[CacheSync] Failed to clear AsyncStorage caches:', error);
42+
}
43+
}
44+
45+
/**
46+
* Sync all cache layers after a note operation
47+
* @param note - The note to sync
48+
* @param clearPreviewCaches - Whether to clear AsyncStorage caches (for delete/archive/move)
49+
*/
50+
export async function syncAllCaches(note: Note, clearPreviewCaches: boolean = false): Promise<void> {
51+
// Always update SQLite cache
52+
await updateSQLiteCache(note);
53+
54+
// Optionally clear AsyncStorage preview caches
55+
if (clearPreviewCaches) {
56+
await clearAsyncStorageCaches();
57+
}
58+
}

0 commit comments

Comments
 (0)