Skip to content

Commit 5d61b0b

Browse files
Merge pull request #326 from alichherawalla/testv/0.0.90
Testv/0.0.90
2 parents 98cb208 + 995a99a commit 5d61b0b

112 files changed

Lines changed: 135994 additions & 5881 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(adb -s 192.168.43.7:46187 shell \"run-as ai.offgridmobile.dev cat databases/RKStorage\")",
5+
"Bash(adb -s 192.168.43.7:46187 shell \"run-as ai.offgridmobile.dev ls -la files/image_models/\")",
6+
"Bash(node -e \"const RNFS = { DocumentDirectoryPath: '[RNFS.DocumentDirectoryPath]' }; console.log\\('Android DocumentDirectoryPath maps to?'\\)\")"
7+
]
8+
}
9+
}

App.tsx

Lines changed: 76 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import { useTheme } from './src/theme';
1414
import { hardwareService, modelManager, authService, ragService, remoteServerManager } from './src/services';
1515
import logger from './src/utils/logger';
1616
import { useAppStore, useAuthStore, useRemoteServerStore } from './src/stores';
17+
import { hydrateDownloadStore } from './src/services/downloadHydration';
18+
import { useDownloads } from './src/hooks/useDownloads';
1719
import { LockScreen } from './src/screens';
1820
import { useAppState } from './src/hooks/useAppState';
21+
import { useDownloadStore } from './src/stores/downloadStore';
1922

2023
LogBox.ignoreAllLogs(); // Suppress all logs
2124

@@ -28,12 +31,12 @@ const ensureRemoteServerStoreHydrated = async () => {
2831
};
2932

3033
function App() {
34+
useDownloads();
3135
const [isInitializing, setIsInitializing] = useState(true);
3236
const setDeviceInfo = useAppStore((s) => s.setDeviceInfo);
3337
const setModelRecommendation = useAppStore((s) => s.setModelRecommendation);
3438
const setDownloadedModels = useAppStore((s) => s.setDownloadedModels);
3539
const setDownloadedImageModels = useAppStore((s) => s.setDownloadedImageModels);
36-
const clearImageModelDownloading = useAppStore((s) => s.clearImageModelDownloading);
3740

3841
const { colors, isDark } = useTheme();
3942

@@ -44,6 +47,27 @@ function App() {
4447
setLastBackgroundTime,
4548
} = useAuthStore();
4649

50+
const reattachTextDownloadRecovery = useCallback(async () => {
51+
const restoredIds = await modelManager.restoreInProgressDownloads();
52+
modelManager.startBackgroundDownloadPolling();
53+
restoredIds.forEach((downloadId) => {
54+
modelManager.watchDownload(
55+
downloadId,
56+
async () => {
57+
const models = await modelManager.getDownloadedModels();
58+
setDownloadedModels(models);
59+
useDownloadStore.getState().remove(
60+
useDownloadStore.getState().downloadIdIndex[downloadId] ?? '',
61+
);
62+
},
63+
(error: Error) => {
64+
logger.error('[App] Restored text download failed:', error);
65+
useDownloadStore.getState().setStatus(downloadId, 'failed', { message: error.message });
66+
},
67+
);
68+
});
69+
}, [setDownloadedModels]);
70+
4771
// Handle app state changes for auto-lock
4872
useAppState({
4973
onBackground: useCallback(() => {
@@ -53,29 +77,39 @@ function App() {
5377
}
5478
}, [authEnabled, setLastBackgroundTime, setLocked]),
5579
onForeground: useCallback(() => {
56-
// Lock is already set when going to background
57-
// Nothing additional needed here
58-
}, []),
80+
// Rebuild the unified store before reattaching JS listeners so restored
81+
// progress events map onto current download entries instead of racing hydration.
82+
hydrateDownloadStore()
83+
.catch((error) => {
84+
logger.error('[App] Failed to hydrate download store on foreground:', error);
85+
})
86+
.finally(() => {
87+
reattachTextDownloadRecovery().catch((error) => {
88+
logger.error('[App] Failed to restore text downloads on foreground:', error);
89+
});
90+
});
91+
}, [reattachTextDownloadRecovery]),
5992
});
6093

61-
useEffect(() => {
62-
initializeApp();
63-
64-
}, []);
65-
66-
const ensureAppStoreHydrated = async () => {
94+
const ensureAppStoreHydrated = useCallback(async () => {
6795
const persistApi = useAppStore.persist;
6896
if (!persistApi?.hasHydrated || !persistApi.rehydrate) return;
6997
if (!persistApi.hasHydrated()) {
7098
await persistApi.rehydrate();
7199
}
72-
};
100+
}, []);
73101

74-
const initializeApp = async () => {
102+
const initializeApp = useCallback(async () => {
75103
try {
76104
// Ensure persisted download metadata is loaded before restore logic reads it.
77105
await ensureAppStoreHydrated();
78106

107+
// Hydrate download store from SQLite before any screen mounts.
108+
await hydrateDownloadStore().catch((error) => {
109+
logger.error('[App] Failed to hydrate download store during startup:', error);
110+
});
111+
await reattachTextDownloadRecovery();
112+
79113
// Phase 1: Quick initialization - get app ready to show UI
80114
// Initialize hardware detection
81115
const deviceInfo = await hardwareService.getDeviceInfo();
@@ -90,84 +124,25 @@ function App() {
90124
// Clean up any mmproj files that were incorrectly added as standalone models
91125
await modelManager.cleanupMMProjEntries();
92126

93-
// Wire up background download metadata persistence
94-
const {
95-
setBackgroundDownload,
96-
activeBackgroundDownloads,
97-
addDownloadedModel,
98-
setDownloadProgress,
99-
} = useAppStore.getState();
100-
modelManager.setBackgroundDownloadMetadataCallback((downloadId, info) => {
101-
setBackgroundDownload(downloadId, info);
127+
// Reconcile image model directories that finished extracting on disk but
128+
// whose AsyncStorage registration was lost to an app kill. Runs before
129+
// refreshModelLists so the recovered models are included in the initial
130+
// setDownloadedImageModels call. activeModelIds guards against touching
131+
// directories that are currently being downloaded/extracted.
132+
const activeImageModelIds = new Set(
133+
Object.values(useDownloadStore.getState().downloads)
134+
.filter(e => e.modelType === 'image')
135+
.map(e => e.modelId.replace('image:', '')),
136+
);
137+
await modelManager.reconcileFinishedImageDownloads(activeImageModelIds).catch((error) => {
138+
logger.error('[App] Image model reconciliation failed:', error);
102139
});
103140

104-
// Recover any background downloads that completed while app was dead
105-
try {
106-
const recoveredModels = await modelManager.syncBackgroundDownloads(
107-
activeBackgroundDownloads,
108-
(downloadId) => setBackgroundDownload(downloadId, null)
109-
);
110-
for (const model of recoveredModels) {
111-
addDownloadedModel(model);
112-
logger.log('[App] Recovered background download:', model.name);
113-
}
114-
} catch (err) {
115-
logger.error('[App] Failed to sync background downloads:', err);
116-
}
117-
118-
// Recover completed image downloads (zip unzip / multifile finalization)
119-
try {
120-
const recoveredImageModels = await modelManager.syncCompletedImageDownloads(
121-
activeBackgroundDownloads,
122-
(downloadId) => setBackgroundDownload(downloadId, null),
123-
);
124-
for (const model of recoveredImageModels) {
125-
logger.log('[App] Recovered image download:', model.name);
126-
}
127-
} catch (err) {
128-
logger.error('[App] Failed to sync completed image downloads:', err);
129-
}
130-
131-
// Re-wire event listeners for downloads that were still running when the
132-
// app was killed (running/pending status in Android DownloadManager).
133-
try {
134-
const restoredDownloadIds = await modelManager.restoreInProgressDownloads(
135-
activeBackgroundDownloads,
136-
(progress) => {
137-
const key = `${progress.modelId}/${progress.fileName}`;
138-
setDownloadProgress(key, {
139-
progress: progress.progress,
140-
bytesDownloaded: progress.bytesDownloaded,
141-
totalBytes: progress.totalBytes,
142-
});
143-
},
144-
);
145-
for (const downloadId of restoredDownloadIds) {
146-
const metadata = activeBackgroundDownloads[downloadId];
147-
const progressKey = metadata ? `${metadata.modelId}/${metadata.fileName}` : null;
148-
modelManager.watchDownload(
149-
downloadId,
150-
(model) => {
151-
if (progressKey) setDownloadProgress(progressKey, null);
152-
addDownloadedModel(model);
153-
logger.log('[App] Restored in-progress download completed:', model.name);
154-
},
155-
(error) => {
156-
if (progressKey) setDownloadProgress(progressKey, null);
157-
logger.error('[App] Restored in-progress download failed:', error);
158-
},
159-
);
160-
}
161-
} catch (err) {
162-
logger.error('[App] Failed to restore in-progress downloads:', err);
163-
}
164-
165-
// Clear any stale imageModelDownloading entries — if the app was killed
166-
// mid-download these would be persisted as "downloading" forever.
167-
clearImageModelDownloading();
168-
169141
// Scan for any models that may have been downloaded externally or
170-
// when app was killed before JS callback fired
142+
// while the app was killed. hydrateDownloadStore (called on cold start
143+
// and foreground resume) repopulates in-flight downloads directly
144+
// from the native Room DB, replacing the old metadata-callback +
145+
// syncBackgroundDownloads recovery path.
171146
const { textModels, imageModels } = await modelManager.refreshModelLists();
172147
setDownloadedModels(textModels);
173148
setDownloadedImageModels(imageModels);
@@ -200,7 +175,20 @@ function App() {
200175
logger.error('[App] Error initializing app:', error);
201176
setIsInitializing(false);
202177
}
203-
};
178+
}, [
179+
authEnabled,
180+
ensureAppStoreHydrated,
181+
reattachTextDownloadRecovery,
182+
setDeviceInfo,
183+
setDownloadedImageModels,
184+
setDownloadedModels,
185+
setLocked,
186+
setModelRecommendation,
187+
]);
188+
189+
useEffect(() => {
190+
initializeApp();
191+
}, [initializeApp]);
204192

205193
const handleUnlock = useCallback(() => {
206194
setLocked(false);

0 commit comments

Comments
 (0)