This document guides the remaining modifications needed to complete the SQLite architecture refactor.
Location: Top of file (around line 1-30)
import { sqliteWriteManager } from './sqliteWriteManager';
import { incrementalDiffer } from './incrementalDiffer';
import { syncPerformanceTracker } from './syncPerformanceTracker';Location: Around line 583-585
BEFORE:
await Promise.all([
acceptedFolders.length > 0 ? saveFoldersToSQLite(acceptedFolders, activeUserId) : Promise.resolve(),
acceptedPlaylists.length > 0 ? savePlaylistsToSQLite(acceptedPlaylists, activeUserId) : Promise.resolve(),
finalAcceptedCards.length > 0 ? saveCardsToSQLite(finalAcceptedCards, activeUserId) : Promise.resolve(),
]);AFTER:
// Use serialized write manager instead of concurrent Promise.all()
if (acceptedFolders.length > 0) {
await sqliteWriteManager.enqueue({
id: `sync-folders-${Date.now()}`,
type: 'folders',
userId: activeUserId,
data: acceptedFolders,
timestamp: Date.now(),
priority: 'normal',
});
}
if (acceptedPlaylists.length > 0) {
await sqliteWriteManager.enqueue({
id: `sync-playlists-${Date.now()}`,
type: 'playlists',
userId: activeUserId,
data: acceptedPlaylists,
timestamp: Date.now(),
priority: 'normal',
});
}
if (finalAcceptedCards.length > 0) {
await sqliteWriteManager.enqueue({
id: `sync-cards-${Date.now()}`,
type: 'cards',
userId: activeUserId,
data: finalAcceptedCards,
timestamp: Date.now(),
priority: 'normal',
dedupeKey: `sync-cards-${activeUserId}`, // Coalesce rapid delta syncs
});
}Location: Around the existing sync try-catch (wrap the main sync logic)
const startSync = Date.now();
const phaseId = syncPerformanceTracker.startPhase('Delta Sync');
try {
// ... existing sync logic ...
syncPerformanceTracker.endPhase(phaseId, 'completed', {
cards: finalAcceptedCards.length,
folders: acceptedFolders.length,
playlists: acceptedPlaylists.length,
});
} catch (err) {
syncPerformanceTracker.endPhase(phaseId, 'failed', {}, err?.message);
throw err;
}Location: If checksum validation exists in syncManager
// Before full resync, try incremental patching
const checksumMismatch = calculatedChecksum !== remoteChecksum;
if (checksumMismatch) {
const diff = incrementalDiffer.generateDiffReport(
{
cards: store.cardsById,
folders: store.foldersById,
playlists: store.playlistsById,
},
{
cards: payload.delta?.cards || [],
folders: payload.delta?.folders || [],
playlists: payload.delta?.playlists || [],
},
{
cards: deletedCardIds,
folders: deletedFolderIds,
playlists: deletedPlaylistIds,
}
);
if (!diff.shouldFallbackToFullResync) {
// Apply incremental patches instead of full resync
const patched = incrementalDiffer.applyIncrementalPatches(...);
// Update state and write only changed entities
store.setState({ cardsById: patched.cards, ... });
} else {
// Fall back to full resync only if too many changes
await executeFullResync();
}
}Location: Top of file (imports section)
import { sqliteWriteManager } from '@/utils/sqliteWriteManager';Location: Every place where a user action calls saveCardsToSQLite, saveFoldersToSQLite, or savePlaylistsToSQLite
Pattern: Find these lines:
await saveCardsToSQLite([card], userId);
await saveFoldersToSQLite([folder], userId);
await savePlaylistsToSQLite([playlist], userId);Replace with (example for card classification):
await sqliteWriteManager.enqueue({
id: `classify-${cardId}-${Date.now()}`,
type: 'cards',
userId: userId,
data: [updatedCard],
timestamp: Date.now(),
priority: 'critical',
dedupeKey: `card:${userId}:${cardId}`, // Coalesce rapid changes to same card
});-
Classification (CLASSIFY_CARD)
dedupeKey: "card:{userId}:{cardId}"priority: 'critical'
-
Favorite Toggle (TOGGLE_FAVORITE)
dedupeKey: "card:{userId}:{cardId}"priority: 'critical'
-
Playlist Operations (CREATE_PLAYLIST, UPDATE_PLAYLIST, DELETE_PLAYLIST)
dedupeKey: "playlist:{userId}:{playlistId}"priority: 'normal'
-
Folder Operations (CREATE_FOLDER, UPDATE_FOLDER, DELETE_FOLDER)
dedupeKey: "folder:{userId}:{folderId}"priority: 'normal'
BEFORE:
const classifyCard = useCallback(async (cardId: string, state: string) => {
// ... state update logic ...
await saveCardsToSQLite([updatedCard], state.userId || 'guest-user');
}, []);AFTER:
const classifyCard = useCallback(async (cardId: string, state: string) => {
const userId = state.userId || 'guest-user';
const cleanId = cardId.split('-loop-')[0];
// ... state update logic ...
await sqliteWriteManager.enqueue({
id: `classify-${cardId}`,
type: 'cards',
userId: userId,
data: [updatedCard],
timestamp: Date.now(),
priority: 'critical',
dedupeKey: `card:${userId}:${cleanId}`, // Coalesce rapid classifications
});
}, []);Search for: withTransactionAsync inside withTransactionAsync
Example Location: rotateQueueEncryptionKey() function
Check if:
await db.withTransactionAsync(async () => {
const rows = await db.getAllAsync(...); // ✅ OK - READ inside transaction
for (const row of rows) {
await db.runAsync(...); // ✅ OK - WRITE inside transaction
}
});DON'T DO:
await db.withTransactionAsync(async () => {
// Some code
await db.withTransactionAsync(async () => { // ❌ NESTED TRANSACTION!
// More code
});
});If you find nested transactions, move the inner one outside or use direct runAsync() inside the outer transaction.
- Write manager serialization (one transaction at a time)
- Coalescing dedupe logic (last write wins)
- Incremental differ accuracy
- Performance tracker metrics collection
- Reseeding doesn't trigger on version match
- Checksum mismatch uses incremental patching
- Full resync only on >30% changes
- User actions (classify, favorite) use write manager
- Zero transaction deadlocks
- Zero WAL conflicts
- Startup time: <500ms
- Write latency: <100ms average
- Delta sync: <500ms
- Memory: no unbounded growth in queue
- All existing sync functionality works
- Offline mode works
- Conflict resolution works
- User state preserved after sync
- Create new files (Write Manager, Differ, Tracker)
- Deploy to canary users (5%)
- Monitor for crashes, errors
- Add write manager calls
- Deploy to canary
- Monitor sync success rate
- Update user action writes
- Feature flag: fall back to old behavior if issues
- Gradual rollout: 10% → 25% → 50% → 100%
- After 2 weeks of stable performance
- Remove fallback code
- Cleanup old transaction patterns
// Check if write manager is working
const metrics = sqliteWriteManager.getMetrics();
console.log('Write Manager Metrics:', metrics);
// Expected: totalOps > 0, coalescedOps > 0, errorCount === 0
// Check performance metrics
const report = syncPerformanceTracker.getDetailedReport();
console.log('Performance Report:', report);
// Expected: totalDuration < 1000ms, failedPhases === 0
// Print beautiful summary
syncPerformanceTracker.logSummary();
// Expected: All phases < 200msIf you don't set dedupeKey, rapid updates won't coalesce. User clicks 10 times → 10 writes.
Always set dedupeKey for user-triggered actions.
Use 'critical' for immediate actions (classify, favorite).
Use 'normal' for background sync.
Use 'low' for nice-to-have operations.
If you don't await the write manager call, state updates before write completes!
Always: await sqliteWriteManager.enqueue(...)
Don't mix saveCardsToSQLite() with sqliteWriteManager.enqueue() in same function.
Choose one pattern per file.
Test rapid user interaction:
// Simulate rapid clicks
for (let i = 0; i < 10; i++) {
classifyCard(cardId, 'easy');
await new Promise(r => setTimeout(r, 50)); // 50ms apart
}
// Check metrics: coalescedOps should be ~9, totalOps should be ~1
const metrics = sqliteWriteManager.getMetrics();
console.assert(metrics.coalescedOps >= 8, 'Coalescing not working!');After Phase 2 implementation:
- ✅ All tests pass (unit, integration, regression)
- ✅ Startup time: <500ms
- ✅ Delta sync: <500ms
- ✅ Write latency: <100ms
- ✅ Zero deadlock errors in logs
- ✅ Zero WAL corruption errors
- ✅ 100% sync success rate
- ✅ No user-facing stalls
- Check
SQLITE_ARCHITECTURE_REFACTOR.mdfor full context - Review
useSyncEngine.tsfor reference implementation - Look at existing write manager calls for patterns
- Check performance tracker logs for metrics