diff --git a/mobile/App.tsx b/mobile/App.tsx
index cca7ca6..9304cf6 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,10 +1,11 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
import { ActivityIndicator, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { LoginScreen } from './app/login';
import { RegisterScreen } from './app/register';
@@ -20,9 +21,22 @@ import { FolderScreen } from './app/folder';
import { OnboardingScreen } from './app/onboarding';
import { SyncDetailScreen } from './app/sync-detail';
import { onboardingStorage } from './services/onboardingStorage';
+import { initDB } from './services/fileStore';
+import { migrateFromLegacy } from './services/fileStore/migrate';
+import { createMMKVPersister } from './services/mmkvPersister';
+
+initDB();
+migrateFromLegacy();
const Stack = createNativeStackNavigator();
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ gcTime: 1000 * 60 * 60 * 24, // 24h
+ },
+ },
+});
+const persister = createMMKVPersister();
function AppNavigator() {
const { user, isLoading } = useAuth();
@@ -83,12 +97,18 @@ function AppNavigator() {
export default function App() {
return (
-
+
-
+
);
}
diff --git a/mobile/app.json b/mobile/app.json
index e4666b8..811a563 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -45,7 +45,8 @@
"videosPermission": "VaultDrop a besoin d'accéder à vos vidéos pour les afficher et les organiser."
}
],
- "expo-image"
+ "expo-image",
+ "expo-sqlite"
]
}
}
diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx
index b8b956f..4c3621b 100644
--- a/mobile/app/file-detail.tsx
+++ b/mobile/app/file-detail.tsx
@@ -14,9 +14,8 @@ import {
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { RouteProp, useRoute } from '@react-navigation/native';
-import { useFile, useFileImage } from '../hooks/useFiles';
-import { useDownloadFile } from '../hooks/useUnifiedFiles';
-import { localFileRegistry } from '../services/localFileRegistry';
+import { useFile, useFileImage, useDownloadFile } from '../hooks/useFiles';
+import { fileStore } from '../services/fileStore';
import { TagChip } from '../components/TagChip';
import { FileThumbnail } from '../components/FileThumbnail';
import { SyncStatusBadge } from '../components/SyncStatusBadge';
@@ -53,7 +52,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
const uri = isDevice ? deviceFile.localUri : (imageData?.data?.url);
const file = fileData as any;
- const localEntry = localFileRegistry.getByBackendId(fileId);
+ const localEntry = fileStore.getByBackendId(fileId);
const syncStatus: SyncStatus = isDevice
? 'local'
: localEntry
@@ -77,6 +76,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
mimeType: file.mimeType ?? 'application/octet-stream',
size: file.data?.size ?? 0,
createdAt: file.createdAt ?? new Date().toISOString(),
+ source: 'cloud',
syncStatus: 'cloud',
tags: file.data?.tags ?? [],
isFolder: false,
diff --git a/mobile/app/folder.tsx b/mobile/app/folder.tsx
index 5ab84b2..75586bf 100644
--- a/mobile/app/folder.tsx
+++ b/mobile/app/folder.tsx
@@ -3,11 +3,16 @@ import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Alert }
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialIcons } from '@expo/vector-icons';
-import { useDeleteFile, useFileImage } from '../hooks/useFiles';
-import { useUnifiedFilesByParent } from '../hooks/useUnifiedFiles';
-import { UnifiedFileItem } from '../hooks/useUnifiedFiles';
+import { useDeleteFile, useFileImage, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
+import { UnifiedFileItem } from '../types';
import { isFolder } from '../types';
import { FileThumbnail } from '../components/FileThumbnail';
+import { fileStore } from '../services/fileStore';
+import { downloadRegistry } from '../services/downloadRegistry';
+import { apiClient } from '../api/client';
+import { ENDPOINTS } from '../constants/api';
+import { useQueryClient } from '@tanstack/react-query';
+import { deleteAsync } from 'expo-file-system/legacy';
const NUM_COLUMNS = 3;
const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -65,8 +70,10 @@ export function FolderScreen() {
const route = useRoute();
const navigation = useNavigation();
const { folderId, folderName } = route.params;
- const { data, isLoading } = useUnifiedFilesByParent(folderId);
+ const { data, isLoading } = useFiles(folderId);
const deleteFile = useDeleteFile();
+ const freeLocalSpace = useFreeLocalSpace();
+ const queryClient = useQueryClient();
const [selectedIds, setSelectedIds] = useState>(new Set());
const selectionMode = selectedIds.size > 0;
@@ -76,7 +83,7 @@ export function FolderScreen() {
}, [navigation, folderName]);
const files = useMemo(() => {
- return data ?? [];
+ return data?.data ?? [];
}, [data]);
const fileIdToIndex = useMemo(() => {
@@ -99,26 +106,54 @@ export function FolderScreen() {
}, []);
const handleDelete = useCallback(() => {
- const count = selectedIds.size;
- if (count === 0) return;
- Alert.alert(
- 'Supprimer',
- `Supprimer ${count === 1 ? 'ce fichier' : `ces ${count} fichiers`} ?`,
- [
- { text: 'Annuler', style: 'cancel' },
- {
- text: 'Supprimer',
- style: 'destructive',
- onPress: async () => {
- for (const id of selectedIds) {
- await deleteFile.mutateAsync(id);
- }
- setSelectedIds(new Set());
- },
+ const ids = Array.from(selectedIds);
+ if (ids.length === 0) return;
+ const hasSynced = ids.some((id) => {
+ const f = files.find((fi) => fi.id === id);
+ return f?.syncStatus === 'synced';
+ });
+ const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
+ const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [
+ { text: 'Annuler', style: 'cancel' },
+ ];
+ if (hasSynced) {
+ options.push({
+ text: 'Du device uniquement',
+ onPress: async () => {
+ const syncedIds = ids.filter((id) => {
+ const f = files.find((fi) => fi.id === id);
+ return f?.syncStatus === 'synced';
+ });
+ if (syncedIds.length > 0) {
+ await freeLocalSpace.mutateAsync(syncedIds);
+ }
+ setSelectedIds(new Set());
},
- ]
- );
- }, [selectedIds, deleteFile]);
+ });
+ }
+ options.push({
+ text: 'Du device + serveur',
+ style: 'destructive',
+ onPress: async () => {
+ for (const id of ids) {
+ const f = files.find((fi) => fi.id === id);
+ if (f?.backendFileId) {
+ await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
+ fileStore.deleteByBackendId(f.backendFileId);
+ } else {
+ fileStore.deleteById(id);
+ }
+ if (f?.localUri) {
+ try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
+ }
+ downloadRegistry.remove(id);
+ }
+ await queryClient.invalidateQueries({ queryKey: ['files'] });
+ setSelectedIds(new Set());
+ },
+ });
+ Alert.alert('Supprimer', `Supprimer ${label} ?`, options);
+ }, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
const handleItemPress = useCallback((file: UnifiedFileItem) => {
if (selectionMode) {
diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx
index f4794a2..4354238 100644
--- a/mobile/app/index.tsx
+++ b/mobile/app/index.tsx
@@ -4,10 +4,8 @@ import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialIcons } from '@expo/vector-icons';
-import { useDeleteFile, useAddTags, useMoveFiles, useFolders } from '../hooks/useFiles';
-import { useUnifiedFiles, useFreeLocalSpace } from '../hooks/useUnifiedFiles';
-import { UnifiedFileItem } from '../hooks/useUnifiedFiles';
-import { FileItem, isFolder } from '../types';
+import { useDeleteFile, useAddTags, useMoveFiles, useFolders, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
+import { UnifiedFileItem, isFolder } from '../types';
import { SearchBar, SearchFilters } from '../components/SearchBar';
import { FileThumbnail } from '../components/FileThumbnail';
import { SettingsModal } from '../components/SettingsModal';
@@ -15,6 +13,13 @@ import { SyncStatusIcon } from '../components/SyncStatusIcon';
import { useSyncQueue } from '../hooks/useSyncQueue';
import { useAutoSync } from '../hooks/useAutoSync';
import { safDirectory, StoredFolder, SyncMode, SyncGlobalMode } from '../services/safDirectory';
+import { fileStore } from '../services/fileStore';
+import { downloadRegistry } from '../services/downloadRegistry';
+import { useQueryClient } from '@tanstack/react-query';
+import { apiClient } from '../api/client';
+import { ENDPOINTS } from '../constants/api';
+import { useLocalFiles } from '../hooks/useLocalFiles';
+import { deleteAsync } from 'expo-file-system/legacy';
const NUM_COLUMNS = 3;
const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -157,9 +162,11 @@ function matchesQuery(file: UnifiedFileItem, query: string, filters: SearchFilte
export function HomeScreen() {
const navigation = useNavigation();
const insets = useSafeAreaInsets();
- const { data, isLoading, error, hasPermission, requestPermission, pickDirectory, folders, refreshFolders } = useUnifiedFiles();
+ const { data, isLoading, error } = useFiles();
+ const { hasPermission, requestPermission, pickDirectory, folders, refreshFolders } = useLocalFiles();
const deleteFile = useDeleteFile();
const freeLocalSpace = useFreeLocalSpace();
+ const queryClient = useQueryClient();
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState({ name: true, ocrText: true, tags: true });
const [keyboardOpen, setKeyboardOpen] = useState(false);
@@ -203,7 +210,7 @@ export function HomeScreen() {
const selectionMode = selectedIds.size > 0;
- const files = data ?? [];
+ const files = data?.data ?? [];
const filteredFiles = useMemo(
() => searchQuery.trim() ? files.filter((f) => matchesQuery(f, searchQuery, filters)) : files,
@@ -233,55 +240,54 @@ export function HomeScreen() {
}, []);
const handleDelete = useCallback(() => {
- const count = selectedIds.size;
- if (count === 0) return;
- const label = count === 1 ? 'ce fichier' : `ces ${count} fichiers`;
- Alert.alert(
- 'Supprimer',
- `Supprimer ${label} ?`,
- [
- { text: 'Annuler', style: 'cancel' },
- {
- text: 'Supprimer',
- style: 'destructive',
- onPress: async () => {
- const ids = Array.from(selectedIds);
- for (const id of ids) {
- await deleteFile.mutateAsync(id);
- }
- setSelectedIds(new Set());
- },
- },
- ]
- );
- }, [selectedIds, deleteFile]);
-
- const handleFreeSpace = useCallback(() => {
const ids = Array.from(selectedIds);
- const syncedIds = ids.filter((id) => {
+ if (ids.length === 0) return;
+ const hasSynced = ids.some((id) => {
const f = files.find((fi) => fi.id === id);
return f?.syncStatus === 'synced';
});
- if (syncedIds.length === 0) {
- Alert.alert('Info', 'Aucun fichier avec copie locale sélectionné.');
- return;
- }
- const label = syncedIds.length === 1 ? 'ce fichier' : `ces ${syncedIds.length} fichiers`;
- Alert.alert(
- 'Libérer l\'espace',
- `${label} sera supprimé de votre appareil mais restera disponible sur le serveur. Vous pourrez le re-télécharger ultérieurement.`,
- [
- { text: 'Annuler', style: 'cancel' },
- {
- text: 'Libérer',
- onPress: async () => {
+ const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
+ const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [
+ { text: 'Annuler', style: 'cancel' },
+ ];
+ if (hasSynced) {
+ options.push({
+ text: 'Du device uniquement',
+ onPress: async () => {
+ const syncedIds = ids.filter((id) => {
+ const f = files.find((fi) => fi.id === id);
+ return f?.syncStatus === 'synced';
+ });
+ if (syncedIds.length > 0) {
await freeLocalSpace.mutateAsync(syncedIds);
- setSelectedIds(new Set());
- },
+ }
+ setSelectedIds(new Set());
},
- ]
- );
- }, [selectedIds, files, freeLocalSpace]);
+ });
+ }
+ options.push({
+ text: 'Du device + serveur',
+ style: 'destructive',
+ onPress: async () => {
+ for (const id of ids) {
+ const f = files.find((fi) => fi.id === id);
+ if (f?.backendFileId) {
+ await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
+ fileStore.deleteByBackendId(f.backendFileId);
+ } else {
+ fileStore.deleteById(id);
+ }
+ if (f?.localUri) {
+ try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
+ }
+ downloadRegistry.remove(id);
+ }
+ await queryClient.invalidateQueries({ queryKey: ['files'] });
+ setSelectedIds(new Set());
+ },
+ });
+ Alert.alert('Supprimer', `Supprimer ${label} ?`, options);
+ }, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
const handleEdit = useCallback(() => {
const ids = Array.from(selectedIds);
@@ -542,13 +548,6 @@ export function HomeScreen() {
Déplacer
-
-
- Libérer
-
) : (
@@ -619,7 +618,7 @@ export function HomeScreen() {
Racine
- {(foldersData?.data ?? []).map((folder) => (
+ {(foldersData ?? []).map((folder) => (
{
- return localFileRegistry.getAll().filter(
- (entry) => !entry.backendFileId && entry.syncStatus === 'local'
- );
+ return fileStore.getPendingSync();
}, []);
const handleSyncAll = useCallback(() => {
@@ -36,7 +33,7 @@ export function SyncDetailScreen() {
console.log(`[SyncDetail] sync demandé pour ${pendingFiles.length} fichiers`);
}, [pendingFiles.length]);
- const renderItem = useCallback(({ item }: { item: LocalFileEntry }) => (
+ const renderItem = useCallback(({ item }: { item: FileRecord }) => (
diff --git a/mobile/hooks/useAutoSync.ts b/mobile/hooks/useAutoSync.ts
index 0ea00b2..4171f98 100644
--- a/mobile/hooks/useAutoSync.ts
+++ b/mobile/hooks/useAutoSync.ts
@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { File, UploadType } from 'expo-file-system';
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
import { safDirectory, StoredFolder } from '../services/safDirectory';
-import { localFileRegistry } from '../services/localFileRegistry';
+import { fileStore } from '../services/fileStore';
import { apiClient } from '../api/client';
import { API_BASE_URL, ENDPOINTS } from '../constants/api';
import { ApiError } from '../types';
@@ -70,13 +70,13 @@ export function useAutoSync() {
if (!canSyncBasedOnNetwork(netInfo, globalCellular)) return;
- const registry = localFileRegistry.getAll();
+ const registry = fileStore.getAllLocal();
let pendingFiles = registry.filter(
- (entry) => !entry.backendFileId && entry.syncStatus === 'local' && entry.localUri
+ (entry) => !entry.backendId && entry.syncStatus === 'local' && entry.localUri
);
if (globalMode === 'auto') {
- // Mode auto global : tous les fichiers locaux sans backendFileId
+ // Mode auto global : tous les fichiers locaux sans backendId
// (pas de filtre par dossier)
} else {
// Mode manuel : uniquement les fichiers des dossiers en mode auto
@@ -85,7 +85,7 @@ export function useAutoSync() {
allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
);
pendingFiles = pendingFiles.filter(
- (entry) => entry.folderId && autoFolderIds.has(entry.folderId)
+ (entry) => entry.parentFileId && autoFolderIds.has(entry.parentFileId)
);
}
@@ -96,15 +96,15 @@ export function useAutoSync() {
for (const entry of pendingFiles) {
try {
const uploaded = await uploadFile({
- uri: entry.localUri,
+ uri: entry.localUri!,
type: entry.mimeType,
name: entry.name,
});
- localFileRegistry.register({
- ...entry,
- backendFileId: uploaded.id,
+ fileStore.updatePartial(entry.id, {
+ backendId: uploaded.id,
syncStatus: 'synced',
+ source: 'synced',
});
} catch (err) {
// upload failed, will retry on next cycle
diff --git a/mobile/hooks/useDeviceFiles.ts b/mobile/hooks/useDeviceFiles.ts
index 1381d86..578f0f7 100644
--- a/mobile/hooks/useDeviceFiles.ts
+++ b/mobile/hooks/useDeviceFiles.ts
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import * as MediaLibrary from 'expo-media-library/legacy';
import * as FileSystem from 'expo-file-system/legacy';
-import { LocalFileEntry } from '../types';
import { safDirectory, type StoredFolder } from '../services/safDirectory';
import { downloadRegistry } from '../services/downloadRegistry';
import { useFileWatcher } from './useFileWatcher';
@@ -238,14 +237,3 @@ export function useDeviceFiles() {
return { files, isLoading, hasPermission, requestPermission, rescan, pickDirectory, folders, refreshFolders };
}
-export function deviceFileToLocalEntry(deviceFile: DeviceFile): LocalFileEntry {
- return {
- id: deviceFile.id,
- localUri: deviceFile.uri,
- name: deviceFile.name,
- mimeType: deviceFile.mimeType,
- size: deviceFile.size,
- syncStatus: 'local',
- createdAt: deviceFile.createdAt,
- };
-}
diff --git a/mobile/hooks/useFiles.ts b/mobile/hooks/useFiles.ts
index a3eb217..38d41e1 100644
--- a/mobile/hooks/useFiles.ts
+++ b/mobile/hooks/useFiles.ts
@@ -1,25 +1,108 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
-import { FileItem, PaginatedResponse } from '../types';
-import { metadataCache } from '../services/metadataCache';
+import type { UnifiedFileItem, PaginatedResponse, FileItem, Tag } from '../types';
+import { fileStore } from '../services/fileStore';
+
+function recordToUnifiedItem(record: ReturnType): UnifiedFileItem | null {
+ if (!record) return null;
+ return {
+ id: record.id,
+ backendFileId: record.backendId ?? undefined,
+ name: record.name,
+ mimeType: record.mimeType,
+ size: record.size,
+ createdAt: record.createdAt,
+ updatedAt: record.updatedAt,
+ source: record.source as UnifiedFileItem['source'],
+ syncStatus: record.syncStatus as UnifiedFileItem['syncStatus'],
+ localUri: record.localUri ?? undefined,
+ ocrText: record.ocrText ?? undefined,
+ tags: record.tags ?? [],
+ isFolder: record.isFolder === 1,
+ parentFileId: record.parentFileId ?? undefined,
+ thumbnailUrl: record.thumbnailUrl ?? undefined,
+ isDeviceFile: record.source === 'local' && !record.backendId,
+ };
+}
+
+function recordsToUnifiedItems(records: ReturnType): {
+ data: UnifiedFileItem[];
+ meta: { page: number; total: number };
+} {
+ return {
+ data: records.files.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
+ meta: { page: 0, total: records.total },
+ };
+}
+
+export function useFiles(parentId?: string | null, page: number = 1, limit: number = 50) {
+ const queryKey = parentId
+ ? ['files', parentId]
+ : ['files', 'root', page, limit];
-export function useFiles(page: number = 1, limit: number = 20) {
return useQuery({
- queryKey: ['files', page, limit],
+ queryKey,
queryFn: async () => {
- const res = await apiClient.get>(
- `${ENDPOINTS.FILES}?page=${page}&limit=${limit}&thumbnail=thumbnail`
+ if (parentId) {
+ const backendRes = await apiClient.get<{ data: FileItem[] }>(
+ `/files/folders/${parentId}/files?thumbnail=thumbnail`,
+ );
+ fileStore.mergeFromBackend(
+ backendRes.data.map((f) => ({
+ id: f.id,
+ name: f.name,
+ mimeType: f.mimeType,
+ size: f.size,
+ createdAt: f.createdAt,
+ updatedAt: f.updatedAt,
+ ocrText: f.ocrText,
+ tags: f.tags,
+ isFolder: f.isFolder,
+ parentFileId: f.parentFileId,
+ thumbnailUrl: f.thumbnailUrl,
+ })),
+ );
+
+ const children = fileStore.getChildrenByParent(parentId);
+ return {
+ data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
+ meta: { page: 0, total: children.length },
+ };
+ }
+
+ const backendRes = await apiClient.get>(
+ `${ENDPOINTS.FILES}?page=${page}&limit=${limit}&thumbnail=thumbnail`,
);
- metadataCache.setFiles(res.data, res.meta.page, res.meta.total);
- return res;
+ fileStore.mergeFromBackend(
+ backendRes.data.map((f) => ({
+ id: f.id,
+ name: f.name,
+ mimeType: f.mimeType,
+ size: f.size,
+ createdAt: f.createdAt,
+ updatedAt: f.updatedAt,
+ ocrText: f.ocrText,
+ tags: f.tags,
+ isFolder: f.isFolder,
+ parentFileId: f.parentFileId,
+ thumbnailUrl: f.thumbnailUrl,
+ })),
+ );
+
+ const cached = fileStore.getPaginated(page, limit);
+ return recordsToUnifiedItems(cached);
},
initialData: () => {
- const cached = metadataCache.getFiles();
- if (cached && cached.page === page) {
- return { data: cached.files, meta: { page: cached.page, total: cached.total } };
+ let records;
+ if (parentId) {
+ const children = fileStore.getChildrenByParent(parentId);
+ records = { files: children, total: children.length };
+ } else {
+ records = fileStore.getPaginated(page, limit);
}
- return undefined;
+ if (records.files.length === 0) return undefined;
+ return recordsToUnifiedItems(records);
},
staleTime: 30_000,
});
@@ -36,7 +119,10 @@ export function useFile(id: string) {
export function useFileImage(fileId: string) {
return useQuery({
queryKey: ['fileImage', fileId],
- queryFn: () => apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(`${ENDPOINTS.FILE}/${fileId}`),
+ queryFn: () =>
+ apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(
+ `${ENDPOINTS.FILE}/${fileId}`,
+ ),
enabled: !!fileId,
});
}
@@ -45,10 +131,13 @@ export function useDeleteFile() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (id: string) => apiClient.delete(`${ENDPOINTS.FILES}/${id}`),
+ mutationFn: async (id: string) => {
+ const result = await apiClient.delete(`${ENDPOINTS.FILES}/${id}`);
+ fileStore.deleteByBackendId(id);
+ return result;
+ },
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
- metadataCache.clear();
},
});
}
@@ -61,7 +150,6 @@ export function useAddTags() {
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
- metadataCache.clear();
},
});
}
@@ -74,7 +162,6 @@ export function useMoveFiles() {
apiClient.post(ENDPOINTS.MOVE, { file_ids: fileIds, parent_file_id: parentFileId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
- metadataCache.clear();
},
});
}
@@ -82,15 +169,112 @@ export function useMoveFiles() {
export function useFolders() {
return useQuery({
queryKey: ['folders'],
- queryFn: () => apiClient.get<{ data: FileItem[] }>(ENDPOINTS.FOLDERS),
+ queryFn: async () => {
+ const backendRes = await apiClient.get<{ data: FileItem[] }>(ENDPOINTS.FOLDERS);
+ fileStore.mergeFromBackend(
+ backendRes.data.map((f) => ({
+ id: f.id,
+ name: f.name,
+ mimeType: f.mimeType,
+ size: f.size,
+ createdAt: f.createdAt,
+ updatedAt: f.updatedAt,
+ ocrText: f.ocrText,
+ tags: f.tags,
+ isFolder: f.isFolder,
+ parentFileId: f.parentFileId,
+ thumbnailUrl: f.thumbnailUrl,
+ })),
+ );
+ return fileStore.getAllFolders();
+ },
+ initialData: () => {
+ const folders = fileStore.getAllFolders();
+ return folders.length > 0 ? folders : undefined;
+ },
+ staleTime: 60_000,
});
}
export function useFilesByParent(parentId: string) {
- return useQuery({
- queryKey: ['files', 'parent', parentId],
- queryFn: () =>
- apiClient.get<{ data: FileItem[] }>(`${ENDPOINTS.FOLDERS}/${parentId}/files?thumbnail=thumbnail`),
- enabled: !!parentId,
+ return useFiles(parentId);
+}
+
+export function useDownloadFile() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (file: UnifiedFileItem): Promise => {
+ if (file.syncStatus !== 'cloud') {
+ return file.localUri ?? '';
+ }
+
+ const res = await apiClient.get<{ data: { url: string; name: string } }>(
+ `/files/${file.backendFileId}`,
+ );
+ const downloadUrl = res.data.url;
+
+ const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
+ const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
+ await makeDirectoryAsync(DOWNLOAD_DIR, { intermediates: true });
+
+ let hash = 0;
+ for (let i = 0; i < file.name.length; i++) {
+ hash = ((hash << 5) - hash + file.name.charCodeAt(i)) | 0;
+ }
+ const cacheKey = Math.abs(hash).toString(36);
+ const dot = file.name.lastIndexOf('.');
+ const ext = dot >= 0 ? file.name.slice(dot) : '';
+ const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
+
+ const result = await downloadAsync(downloadUrl, fileUri);
+
+ fileStore.upsert({
+ id: file.backendFileId ?? file.id,
+ backendId: file.backendFileId ?? file.id,
+ name: file.name,
+ mimeType: file.mimeType,
+ size: file.size,
+ source: 'synced',
+ localUri: result.uri,
+ syncStatus: 'synced',
+ parentFileId: file.parentFileId ?? null,
+ isFolder: 0,
+ ocrText: file.ocrText ?? null,
+ thumbnailUrl: file.thumbnailUrl ?? null,
+ createdAt: file.createdAt,
+ updatedAt: file.updatedAt ?? file.createdAt,
+ lastSyncedAt: new Date().toISOString(),
+ tags: file.tags,
+ });
+
+ queryClient.invalidateQueries({ queryKey: ['files'] });
+ return result.uri;
+ },
+ });
+}
+
+export function useFreeLocalSpace() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (fileIds: string[]) => {
+ const { deleteAsync } = await import('expo-file-system/legacy');
+ for (const fid of fileIds) {
+ const entry = fileStore.getByBackendId(fid);
+ if (!entry) continue;
+
+ if (entry.localUri) {
+ try {
+ await deleteAsync(entry.localUri, { idempotent: true });
+ } catch {}
+ }
+
+ fileStore.markAsCloudOnly(entry.id);
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['files'] });
+ },
});
}
diff --git a/mobile/hooks/useLocalFiles.ts b/mobile/hooks/useLocalFiles.ts
index 9a1ecb7..5ca832b 100644
--- a/mobile/hooks/useLocalFiles.ts
+++ b/mobile/hooks/useLocalFiles.ts
@@ -1,7 +1,7 @@
import { useMemo, useEffect, useRef } from 'react';
import { useDeviceFiles } from './useDeviceFiles';
-import { localFileRegistry } from '../services/localFileRegistry';
-import { LocalFileEntry } from '../types';
+import { fileStore } from '../services/fileStore';
+import { UnifiedFileItem } from '../types';
export function useLocalFiles() {
const { files: deviceFiles, isLoading: deviceLoading, hasPermission, requestPermission, rescan, pickDirectory, folders, refreshFolders } = useDeviceFiles();
@@ -12,44 +12,56 @@ export function useLocalFiles() {
if (deviceFiles.length === lastDeviceCount.current) return;
lastDeviceCount.current = deviceFiles.length;
- const newEntries: LocalFileEntry[] = [];
- for (const df of deviceFiles) {
- if (localFileRegistry.get(df.id)) continue;
- newEntries.push({
+ fileStore.mergeFromDevice(
+ deviceFiles.map((df) => ({
id: df.id,
- localUri: df.uri,
+ uri: df.uri,
name: df.name,
mimeType: df.mimeType,
size: df.size,
- syncStatus: 'local',
createdAt: df.createdAt,
folderId: df.folderId,
- });
- }
- if (newEntries.length > 0) {
- localFileRegistry.registerBatch(newEntries);
- }
+ })),
+ );
}, [deviceFiles]);
const localFiles = useMemo(() => {
- const registryEntries = localFileRegistry.getAll();
- const merged = new Map();
+ const registryEntries = fileStore.getAllLocal();
+ const merged = new Map();
for (const entry of registryEntries) {
- merged.set(entry.id, entry);
+ merged.set(entry.id, {
+ id: entry.id,
+ backendFileId: entry.backendId ?? undefined,
+ name: entry.name,
+ mimeType: entry.mimeType,
+ size: entry.size,
+ createdAt: entry.createdAt,
+ source: entry.source as UnifiedFileItem['source'],
+ syncStatus: entry.syncStatus as UnifiedFileItem['syncStatus'],
+ localUri: entry.localUri ?? undefined,
+ tags: entry.tags ?? [],
+ isFolder: entry.isFolder === 1,
+ parentFileId: entry.parentFileId ?? undefined,
+ isDeviceFile: entry.source === 'local' && !entry.backendId,
+ });
}
for (const df of deviceFiles) {
- if (!merged.has(df.id)) {
+ if (!merged.has(df.id) && !fileStore.isDeleted(df.id)) {
merged.set(df.id, {
id: df.id,
- localUri: df.uri,
name: df.name,
mimeType: df.mimeType,
size: df.size,
- syncStatus: 'local',
createdAt: df.createdAt,
- folderId: df.folderId,
+ source: 'local',
+ syncStatus: 'local',
+ localUri: df.uri,
+ tags: [],
+ isFolder: false,
+ isDeviceFile: true,
+ parentFileId: df.folderId,
});
}
}
diff --git a/mobile/hooks/usePullSync.ts b/mobile/hooks/usePullSync.ts
index dc339c8..8dfb716 100644
--- a/mobile/hooks/usePullSync.ts
+++ b/mobile/hooks/usePullSync.ts
@@ -1,10 +1,8 @@
import { useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { downloadAsync, documentDirectory, makeDirectoryAsync } from 'expo-file-system/legacy';
-import { useFiles } from './useFiles';
-import { localFileRegistry } from '../services/localFileRegistry';
+import { fileStore } from '../services/fileStore';
import { apiClient } from '../api/client';
-import { LocalFileEntry } from '../types';
import { setIsSyncing } from './useSyncQueue';
const SYNC_DIR = `${documentDirectory}synced-files/`;
@@ -30,9 +28,9 @@ export function usePullSync() {
}> }>('/files?page=1&limit=100&thumbnail=thumbnail');
const backendFiles = res.data ?? [];
- const registry = localFileRegistry.getAll();
+ const registry = fileStore.getAllSynced();
const existingBackendIds = new Set(
- registry.filter((e) => e.backendFileId).map((e) => e.backendFileId)
+ registry.filter((e) => e.backendId).map((e) => e.backendId)
);
let pulled = 0;
@@ -51,17 +49,23 @@ export function usePullSync() {
const result = await downloadAsync(downloadUrl, fileUri);
- const entry: LocalFileEntry = {
- id: `pull_${bf.id}`,
- backendFileId: bf.id,
- localUri: result.uri,
+ fileStore.upsert({
+ id: bf.id,
+ backendId: bf.id,
name: bf.name,
mimeType: bf.mimeType,
size: bf.size,
+ source: 'synced',
+ localUri: result.uri,
syncStatus: 'synced',
+ parentFileId: null,
+ isFolder: 0,
+ ocrText: null,
+ thumbnailUrl: null,
createdAt: bf.createdAt,
- };
- localFileRegistry.register(entry);
+ updatedAt: bf.createdAt,
+ lastSyncedAt: new Date().toISOString(),
+ });
pulled++;
} catch {
// skip individual file failures
diff --git a/mobile/hooks/useSyncQueue.ts b/mobile/hooks/useSyncQueue.ts
index 1211486..b479975 100644
--- a/mobile/hooks/useSyncQueue.ts
+++ b/mobile/hooks/useSyncQueue.ts
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { createMMKV } from 'react-native-mmkv';
-import { localFileRegistry } from '../services/localFileRegistry';
+import { fileStore } from '../services/fileStore';
import { safDirectory } from '../services/safDirectory';
import { useDeviceFiles } from './useDeviceFiles';
@@ -28,19 +28,16 @@ export function useSyncQueue() {
return;
}
- const registry = localFileRegistry.getAll();
+ const registry = fileStore.getPendingSync();
let count = 0;
for (const entry of registry) {
- if (entry.backendFileId) continue;
- if (entry.syncStatus !== 'local') continue;
-
if (globalMode === 'auto') {
count++;
} else {
// mode manuel : uniquement les fichiers des dossiers en mode auto
- if (!entry.folderId) continue;
- const folder = safDirectory.getAll().find((f) => f.id === entry.folderId);
+ if (!entry.parentFileId) continue;
+ const folder = safDirectory.getAll().find((f) => f.id === entry.parentFileId);
if (folder && folder.syncMode === 'auto') {
count++;
}
diff --git a/mobile/hooks/useUnifiedFiles.ts b/mobile/hooks/useUnifiedFiles.ts
deleted file mode 100644
index 09cd713..0000000
--- a/mobile/hooks/useUnifiedFiles.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import { useMemo } from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { downloadAsync, documentDirectory, makeDirectoryAsync, deleteAsync } from 'expo-file-system/legacy';
-import { useFiles } from './useFiles';
-import { useLocalFiles } from './useLocalFiles';
-import { localFileRegistry } from '../services/localFileRegistry';
-import { thumbnailCache } from '../services/thumbnailCache';
-import { apiClient } from '../api/client';
-import { FileItem, LocalFileEntry, SyncStatus, Tag } from '../types';
-
-export interface UnifiedFileItem {
- id: string;
- backendFileId?: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- updatedAt?: string;
- syncStatus: SyncStatus;
- localUri?: string;
- ocrText?: string;
- tags: Tag[];
- isFolder: boolean;
- parentFileId?: string;
- url?: string;
- thumbnailUrl?: string;
- isDeviceFile?: boolean;
- duplicateOf?: string;
-}
-
-const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
-
-function getCacheKey(name: string): string {
- let hash = 0;
- for (let i = 0; i < name.length; i++) {
- hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
- }
- return Math.abs(hash).toString(36);
-}
-
-function getExtension(name: string): string {
- const dot = name.lastIndexOf('.');
- return dot >= 0 ? name.slice(dot) : '';
-}
-
-function parseExpiresFromUrl(url: string): number {
- try {
- const u = new URL(url);
- const expires = u.searchParams.get('expires');
- if (expires) return Number(expires) * 1000;
- } catch {}
- return Date.now() + 50 * 60 * 1000;
-}
-
-export function useUnifiedFiles(page: number = 1, limit: number = 50) {
- const { data: backendData, isLoading: backendLoading, error: backendError } = useFiles(page, limit);
- const { localFiles, isLoading: localLoading, hasPermission, requestPermission, pickDirectory, folders, refreshFolders } = useLocalFiles();
-
- const unifiedFiles = useMemo(() => {
- const backendFiles = backendData?.data ?? [];
- const merged = new Map();
-
- const registryAll = localFileRegistry.getAll();
- const backendIdToLocal = new Map();
- for (const entry of registryAll) {
- if (entry.backendFileId) {
- backendIdToLocal.set(entry.backendFileId, entry);
- }
- }
-
- const nameSizeIndex = new Map();
- for (const bf of backendFiles) {
- if (!bf.isFolder && bf.size > 0) {
- nameSizeIndex.set(`${bf.name}::${bf.size}`, bf.id);
- }
- }
-
- for (const bf of backendFiles) {
- const localEntry = backendIdToLocal.get(bf.id);
-
- let thumbUrl = bf.thumbnailUrl;
- if (thumbUrl && bf.thumbnailUrl) {
- const expiresAt = parseExpiresFromUrl(bf.thumbnailUrl);
- thumbnailCache.set(bf.id, bf.thumbnailUrl, expiresAt);
- } else {
- const cached = thumbnailCache.get(bf.id);
- if (cached) thumbUrl = cached;
- }
-
- merged.set(bf.id, {
- id: bf.id,
- backendFileId: bf.id,
- name: bf.name,
- mimeType: bf.mimeType,
- size: bf.size,
- createdAt: bf.createdAt,
- updatedAt: bf.updatedAt,
- syncStatus: localEntry ? 'synced' : 'cloud',
- localUri: localEntry?.localUri,
- ocrText: bf.ocrText,
- tags: bf.tags,
- isFolder: bf.isFolder,
- parentFileId: bf.parentFileId,
- url: bf.url,
- thumbnailUrl: thumbUrl,
- });
- }
-
- for (const lf of localFiles) {
- if (lf.backendFileId && merged.has(lf.backendFileId)) continue;
- if (merged.has(lf.id)) continue;
-
- if (!lf.folderId && lf.size > 0) {
- const key = `${lf.name}::${lf.size}`;
- const matchId = nameSizeIndex.get(key);
- if (matchId) {
- const existing = merged.get(matchId);
- if (existing && !existing.localUri && lf.localUri) {
- merged.set(matchId, { ...existing, localUri: lf.localUri, syncStatus: 'synced' });
- }
- continue;
- }
- }
-
- merged.set(lf.id, {
- id: lf.id,
- backendFileId: lf.backendFileId,
- name: lf.name,
- mimeType: lf.mimeType,
- size: lf.size,
- createdAt: lf.createdAt,
- syncStatus: 'local',
- localUri: lf.localUri,
- tags: lf.tags ?? [],
- isFolder: false,
- isDeviceFile: true,
- });
- }
-
- return Array.from(merged.values()).sort((a, b) => {
- const dateA = new Date(a.createdAt).getTime();
- const dateB = new Date(b.createdAt).getTime();
- return dateB - dateA;
- });
- }, [backendData, localFiles]);
-
- return {
- data: unifiedFiles,
- isLoading: backendLoading || localLoading,
- error: backendError,
- hasPermission,
- requestPermission,
- pickDirectory,
- folders,
- refreshFolders,
- };
-}
-
-export function useUnifiedFilesByParent(parentId: string) {
- const { data: backendData, isLoading: backendLoading } = useQuery({
- queryKey: ['files', 'parent', parentId],
- queryFn: () =>
- apiClient.get<{ data: FileItem[] }>(`/files/folders/${parentId}/files?thumbnail=thumbnail`),
- enabled: !!parentId,
- });
-
- const { localFiles, isLoading: localLoading } = useLocalFiles();
-
- const unifiedFiles = useMemo(() => {
- const backendFiles = backendData?.data ?? [];
- const merged = new Map();
-
- const registryAll = localFileRegistry.getAll();
- const backendIdToLocal = new Map();
- for (const entry of registryAll) {
- if (entry.backendFileId) backendIdToLocal.set(entry.backendFileId, entry);
- }
-
- for (const bf of backendFiles) {
- const localEntry = backendIdToLocal.get(bf.id);
-
- let thumbUrl = bf.thumbnailUrl;
- if (thumbUrl && bf.thumbnailUrl) {
- const expiresAt = parseExpiresFromUrl(bf.thumbnailUrl);
- thumbnailCache.set(bf.id, bf.thumbnailUrl, expiresAt);
- } else {
- const cached = thumbnailCache.get(bf.id);
- if (cached) thumbUrl = cached;
- }
-
- merged.set(bf.id, {
- id: bf.id,
- backendFileId: bf.id,
- name: bf.name,
- mimeType: bf.mimeType,
- size: bf.size,
- createdAt: bf.createdAt,
- updatedAt: bf.updatedAt,
- syncStatus: localEntry ? 'synced' : 'cloud',
- localUri: localEntry?.localUri,
- ocrText: bf.ocrText,
- tags: bf.tags,
- isFolder: bf.isFolder,
- parentFileId: bf.parentFileId,
- url: bf.url,
- thumbnailUrl: thumbUrl,
- });
- }
-
- return Array.from(merged.values()).sort((a, b) => {
- if (a.isFolder && !b.isFolder) return -1;
- if (!a.isFolder && b.isFolder) return 1;
- return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
- });
- }, [backendData, localFiles]);
-
- return {
- data: unifiedFiles,
- isLoading: backendLoading || localLoading,
- };
-}
-
-export function useDownloadFile() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (file: UnifiedFileItem): Promise => {
- if (file.syncStatus !== 'cloud') {
- return file.localUri ?? '';
- }
-
- const res = await apiClient.get<{ data: { url: string; name: string } }>(
- `/files/${file.backendFileId}`
- );
- const downloadUrl = res.data.url;
-
- await makeDirectoryAsync(DOWNLOAD_DIR, { intermediates: true });
- const cacheKey = getCacheKey(file.name);
- const ext = getExtension(file.name);
- const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
-
- const result = await downloadAsync(downloadUrl, fileUri);
-
- const entry: LocalFileEntry = {
- id: `local_${file.backendFileId}`,
- backendFileId: file.backendFileId,
- localUri: result.uri,
- name: file.name,
- mimeType: file.mimeType,
- size: file.size,
- syncStatus: 'synced',
- createdAt: file.createdAt,
- tags: file.tags,
- };
- localFileRegistry.register(entry);
-
- queryClient.invalidateQueries({ queryKey: ['files'] });
-
- return result.uri;
- },
- });
-}
-
-export function useFreeLocalSpace() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (fileIds: string[]) => {
- for (const fid of fileIds) {
- const entry = localFileRegistry.get(fid);
- if (!entry) continue;
-
- if (entry.localUri) {
- try {
- await deleteAsync(entry.localUri, { idempotent: true });
- } catch {}
- }
-
- localFileRegistry.markAsCloudOnly(fid);
- }
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['files'] });
- },
- });
-}
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index 927b7b1..4200bf3 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -12,7 +12,10 @@
"@react-native-community/netinfo": "12.0.1",
"@react-navigation/native": "^7.3.8",
"@react-navigation/native-stack": "^7.17.10",
+ "@tanstack/query-async-storage-persister": "^5.101.4",
"@tanstack/react-query": "^5.101.2",
+ "@tanstack/react-query-persist-client": "^5.101.4",
+ "drizzle-orm": "^0.45.2",
"expo": "~57.0.4",
"expo-document-picker": "~57.0.0",
"expo-file-system": "~57.0.0",
@@ -21,6 +24,7 @@
"expo-media-library": "~57.0.3",
"expo-print": "~57.0.0",
"expo-secure-store": "~57.0.0",
+ "expo-sqlite": "~57.0.1",
"expo-status-bar": "~57.0.0",
"react": "19.2.3",
"react-native": "0.86.0",
@@ -36,6 +40,7 @@
},
"devDependencies": {
"@types/react": "~19.2.2",
+ "drizzle-kit": "^0.31.10",
"typescript": "~6.0.3"
}
},
@@ -1177,6 +1182,13 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@drizzle-team/brocli": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -1189,6 +1201,884 @@
"node": ">=0.8.0"
}
},
+ "node_modules/@esbuild-kit/core-utils": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
+ "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.18.20",
+ "source-map-support": "^0.5.21"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/@esbuild-kit/esm-loader": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
+ "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@esbuild-kit/core-utils": "^3.3.2",
+ "get-tsconfig": "^4.7.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@expo/code-signing-certificates": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
@@ -2018,23 +2908,50 @@
"integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"license": "MIT"
},
+ "node_modules/@tanstack/query-async-storage-persister": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.101.4.tgz",
+ "integrity": "sha512-ROenNOVvxIZ1zKsBAiIvvslLo2xD8Me/vFz/tChdmuI7ciU+ET63yRrQ01W899Tx1ayQXdsGQJqYqjjVZk+OBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.4",
+ "@tanstack/query-persist-client-core": "5.101.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tanstack/query-core": {
- "version": "5.101.2",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz",
- "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==",
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
+ "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
- "node_modules/@tanstack/react-query": {
- "version": "5.101.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz",
- "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==",
+ "node_modules/@tanstack/query-persist-client-core": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.101.4.tgz",
+ "integrity": "sha512-Bmu+RfWhwYEyYZEwSeKC0gX4+KTkiEB3yO8oz9BfAY/Jfe3ndz9EnYVhihIZPmWIr8NXayF7JuraNQcrXQQCCg==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.101.2"
+ "@tanstack/query-core": "5.101.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
+ "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.4"
},
"funding": {
"type": "github",
@@ -2044,6 +2961,23 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tanstack/react-query-persist-client": {
+ "version": "5.101.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.101.4.tgz",
+ "integrity": "sha512-CY5i7PPKS/5B8OR2zAs9nCbLpgqI4jN8eBhtAxKRMNHyGcQhmyOy4PkkJrAAXT+ecGj++fBJTgQw6OpyCt/2SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-persist-client-core": "5.101.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.101.4",
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@types/hammerjs": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
@@ -2264,6 +3198,12 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
+ "node_modules/await-lock": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
+ "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
+ "license": "MIT"
+ },
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
@@ -2898,6 +3838,147 @@
"integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==",
"license": "MIT"
},
+ "node_modules/drizzle-kit": {
+ "version": "0.31.10",
+ "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
+ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@drizzle-team/brocli": "^0.10.2",
+ "@esbuild-kit/esm-loader": "^2.5.5",
+ "esbuild": "^0.25.4",
+ "tsx": "^4.21.0"
+ },
+ "bin": {
+ "drizzle-kit": "bin.cjs"
+ }
+ },
+ "node_modules/drizzle-orm": {
+ "version": "0.45.2",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
+ "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@aws-sdk/client-rds-data": ">=3",
+ "@cloudflare/workers-types": ">=4",
+ "@electric-sql/pglite": ">=0.2.0",
+ "@libsql/client": ">=0.10.0",
+ "@libsql/client-wasm": ">=0.10.0",
+ "@neondatabase/serverless": ">=0.10.0",
+ "@op-engineering/op-sqlite": ">=2",
+ "@opentelemetry/api": "^1.4.1",
+ "@planetscale/database": ">=1.13",
+ "@prisma/client": "*",
+ "@tidbcloud/serverless": "*",
+ "@types/better-sqlite3": "*",
+ "@types/pg": "*",
+ "@types/sql.js": "*",
+ "@upstash/redis": ">=1.34.7",
+ "@vercel/postgres": ">=0.8.0",
+ "@xata.io/client": "*",
+ "better-sqlite3": ">=7",
+ "bun-types": "*",
+ "expo-sqlite": ">=14.0.0",
+ "gel": ">=2",
+ "knex": "*",
+ "kysely": "*",
+ "mysql2": ">=2",
+ "pg": ">=8",
+ "postgres": ">=3",
+ "sql.js": ">=1",
+ "sqlite3": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/client-rds-data": {
+ "optional": true
+ },
+ "@cloudflare/workers-types": {
+ "optional": true
+ },
+ "@electric-sql/pglite": {
+ "optional": true
+ },
+ "@libsql/client": {
+ "optional": true
+ },
+ "@libsql/client-wasm": {
+ "optional": true
+ },
+ "@neondatabase/serverless": {
+ "optional": true
+ },
+ "@op-engineering/op-sqlite": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@planetscale/database": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@tidbcloud/serverless": {
+ "optional": true
+ },
+ "@types/better-sqlite3": {
+ "optional": true
+ },
+ "@types/pg": {
+ "optional": true
+ },
+ "@types/sql.js": {
+ "optional": true
+ },
+ "@upstash/redis": {
+ "optional": true
+ },
+ "@vercel/postgres": {
+ "optional": true
+ },
+ "@xata.io/client": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "bun-types": {
+ "optional": true
+ },
+ "expo-sqlite": {
+ "optional": true
+ },
+ "gel": {
+ "optional": true
+ },
+ "knex": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "postgres": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "sql.js": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ }
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2943,6 +4024,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -3207,6 +4330,20 @@
"node": ">=20.16.0"
}
},
+ "node_modules/expo-sqlite": {
+ "version": "57.0.1",
+ "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-57.0.1.tgz",
+ "integrity": "sha512-I6KoUvfGIiROTKxr5D3H+jRIGA/iEvWEtqHK4XMukAA7tVTinz/YdS8zOz6/DdG6vgrNmvF7gyOcbhLinlfxzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "await-lock": "^2.2.2"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-status-bar": {
"version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-57.0.0.tgz",
@@ -3735,6 +4872,21 @@
"node": ">= 0.6"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3762,6 +4914,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/getenv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
@@ -5826,6 +6991,16 @@
"node": ">=8"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/resolve-workspace-root": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz",
@@ -6373,6 +7548,509 @@
"integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==",
"license": "MIT"
},
+ "node_modules/tsx": {
+ "version": "4.23.1",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
+ "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
"node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
diff --git a/mobile/package.json b/mobile/package.json
index c7bf025..de6c8b1 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -7,7 +7,10 @@
"@react-native-community/netinfo": "12.0.1",
"@react-navigation/native": "^7.3.8",
"@react-navigation/native-stack": "^7.17.10",
+ "@tanstack/query-async-storage-persister": "^5.101.4",
"@tanstack/react-query": "^5.101.2",
+ "@tanstack/react-query-persist-client": "^5.101.4",
+ "drizzle-orm": "^0.45.2",
"expo": "~57.0.4",
"expo-document-picker": "~57.0.0",
"expo-file-system": "~57.0.0",
@@ -16,6 +19,7 @@
"expo-media-library": "~57.0.3",
"expo-print": "~57.0.0",
"expo-secure-store": "~57.0.0",
+ "expo-sqlite": "~57.0.1",
"expo-status-bar": "~57.0.0",
"react": "19.2.3",
"react-native": "0.86.0",
@@ -31,6 +35,7 @@
},
"devDependencies": {
"@types/react": "~19.2.2",
+ "drizzle-kit": "^0.31.10",
"typescript": "~6.0.3"
},
"scripts": {
diff --git a/mobile/services/fileStore/index.ts b/mobile/services/fileStore/index.ts
new file mode 100644
index 0000000..9a690a4
--- /dev/null
+++ b/mobile/services/fileStore/index.ts
@@ -0,0 +1,433 @@
+import { drizzle } from 'drizzle-orm/expo-sqlite';
+import * as SQLite from 'expo-sqlite';
+import { eq, like, or, and, desc, asc, sql, isNull } from 'drizzle-orm';
+import { files, fileTags, deletedFiles } from './schema';
+import type { Tag } from '../../types';
+
+const DB_NAME = 'vaultdrop.db';
+
+let _db: ReturnType | null = null;
+let _sqliteDb: SQLite.SQLiteDatabase | null = null;
+
+export function initDB() {
+ if (_db) return _db;
+ _sqliteDb = SQLite.openDatabaseSync(DB_NAME);
+ _sqliteDb.execSync('PRAGMA journal_mode = WAL;');
+ _sqliteDb.execSync('PRAGMA foreign_keys = ON;');
+ _db = drizzle(_sqliteDb);
+
+ _sqliteDb.execSync(`
+ CREATE TABLE IF NOT EXISTS files (
+ id TEXT PRIMARY KEY,
+ backend_id TEXT,
+ name TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ size INTEGER NOT NULL DEFAULT 0,
+ source TEXT NOT NULL DEFAULT 'cloud',
+ local_uri TEXT,
+ sync_status TEXT NOT NULL DEFAULT 'cloud',
+ parent_file_id TEXT,
+ is_folder INTEGER NOT NULL DEFAULT 0,
+ ocr_text TEXT,
+ thumbnail_url TEXT,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ last_synced_at TEXT
+ );
+ CREATE TABLE IF NOT EXISTS file_tags (
+ id TEXT PRIMARY KEY,
+ file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
+ tag_name TEXT NOT NULL,
+ tag_type TEXT NOT NULL DEFAULT 'none'
+ );
+ CREATE INDEX IF NOT EXISTS idx_files_backend_id ON files(backend_id);
+ CREATE INDEX IF NOT EXISTS idx_files_parent_id ON files(parent_file_id);
+ CREATE INDEX IF NOT EXISTS idx_files_source ON files(source);
+ CREATE INDEX IF NOT EXISTS idx_files_is_folder ON files(is_folder);
+ CREATE INDEX IF NOT EXISTS idx_files_sync_status ON files(sync_status);
+ CREATE INDEX IF NOT EXISTS idx_file_tags_file_id ON file_tags(file_id);
+ CREATE TABLE IF NOT EXISTS deleted_files (
+ id TEXT PRIMARY KEY,
+ deleted_at TEXT NOT NULL
+ );
+ `);
+
+ return _db;
+}
+
+function getDb() {
+ if (!_db) initDB();
+ return _db!;
+}
+
+export type FileRecord = {
+ id: string;
+ backendId: string | null;
+ name: string;
+ mimeType: string;
+ size: number;
+ source: string;
+ localUri: string | null;
+ syncStatus: string;
+ parentFileId: string | null;
+ isFolder: number;
+ ocrText: string | null;
+ thumbnailUrl: string | null;
+ createdAt: string;
+ updatedAt: string;
+ lastSyncedAt: string | null;
+ tags?: Tag[];
+};
+
+type FileRow = typeof files.$inferSelect;
+type TagRow = typeof fileTags.$inferSelect;
+
+function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
+ return {
+ id: row.id,
+ backendId: row.backendId,
+ name: row.name,
+ mimeType: row.mimeType,
+ size: row.size,
+ source: row.source,
+ localUri: row.localUri,
+ syncStatus: row.syncStatus,
+ parentFileId: row.parentFileId,
+ isFolder: row.isFolder,
+ ocrText: row.ocrText,
+ thumbnailUrl: row.thumbnailUrl,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ lastSyncedAt: row.lastSyncedAt,
+ tags,
+ };
+}
+
+function getTagsForFile(fileId: string): Tag[] {
+ const d = getDb();
+ const rows = d.select().from(fileTags).where(eq(fileTags.fileId, fileId)).all();
+ return rows.map((r) => ({ id: r.tagName, tag_name: r.tagName, tag_type: r.tagType }));
+}
+
+function setTagsForFile(fileId: string, tags: Tag[]) {
+ const d = getDb();
+ d.delete(fileTags).where(eq(fileTags.fileId, fileId)).run();
+ if (tags.length === 0) return;
+ d.insert(fileTags).values(
+ tags.map((t) => ({
+ id: `${fileId}_${t.id || t.tag_name}`,
+ fileId,
+ tagName: t.tag_name,
+ tagType: t.tag_type,
+ })),
+ ).run();
+}
+
+function upsertRow(file: FileRecord) {
+ const d = getDb();
+ d.insert(files).values({
+ id: file.id,
+ backendId: file.backendId,
+ name: file.name,
+ mimeType: file.mimeType,
+ size: file.size,
+ source: file.source,
+ localUri: file.localUri,
+ syncStatus: file.syncStatus,
+ parentFileId: file.parentFileId,
+ isFolder: file.isFolder,
+ ocrText: file.ocrText,
+ thumbnailUrl: file.thumbnailUrl,
+ createdAt: file.createdAt,
+ updatedAt: file.updatedAt,
+ lastSyncedAt: file.lastSyncedAt,
+ }).onConflictDoUpdate({
+ target: files.id,
+ set: {
+ backendId: file.backendId,
+ name: file.name,
+ mimeType: file.mimeType,
+ size: file.size,
+ source: file.source,
+ localUri: file.localUri,
+ syncStatus: file.syncStatus,
+ parentFileId: file.parentFileId,
+ isFolder: file.isFolder,
+ ocrText: file.ocrText,
+ thumbnailUrl: file.thumbnailUrl,
+ updatedAt: file.updatedAt,
+ lastSyncedAt: file.lastSyncedAt,
+ },
+ }).run();
+}
+
+export const fileStore = {
+ initDB,
+
+ upsert(file: FileRecord) {
+ upsertRow(file);
+ if (file.tags) setTagsForFile(file.id, file.tags);
+ },
+
+ upsertBatch(fileList: FileRecord[]) {
+ const d = getDb();
+ for (const file of fileList) {
+ upsertRow(file);
+ if (file.tags) setTagsForFile(file.id, file.tags);
+ }
+ },
+
+ getById(id: string): FileRecord | null {
+ const d = getDb();
+ const row = d.select().from(files).where(eq(files.id, id)).get();
+ if (!row) return null;
+ return rowToRecord(row, getTagsForFile(id));
+ },
+
+ getByBackendId(backendId: string): FileRecord | null {
+ const d = getDb();
+ const row = d.select().from(files).where(eq(files.backendId, backendId)).get();
+ if (!row) return null;
+ return rowToRecord(row, getTagsForFile(row.id));
+ },
+
+ getRootFolders(): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(and(eq(files.isFolder, 1), isNull(files.parentFileId)))
+ .orderBy(asc(files.name))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ getChildrenByParent(parentId: string): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(eq(files.parentFileId, parentId))
+ .orderBy(desc(files.isFolder), desc(files.createdAt))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ getPaginated(page: number, limit: number): { files: FileRecord[]; total: number } {
+ const d = getDb();
+ const offset = (page - 1) * limit;
+
+ const countRow = d.select({ count: sql`count(*)` })
+ .from(files)
+ .where(and(isNull(files.parentFileId), eq(files.isFolder, 0)))
+ .get();
+ const total = countRow?.count ?? 0;
+
+ const rows = d.select().from(files)
+ .where(and(isNull(files.parentFileId), eq(files.isFolder, 0)))
+ .orderBy(desc(files.createdAt))
+ .limit(limit)
+ .offset(offset)
+ .all();
+
+ return {
+ files: rows.map((r) => rowToRecord(r, getTagsForFile(r.id))),
+ total,
+ };
+ },
+
+ getAllFolders(): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(eq(files.isFolder, 1))
+ .orderBy(asc(files.name))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ search(query: string): FileRecord[] {
+ const d = getDb();
+ const pattern = `%${query}%`;
+ const rows = d.select().from(files)
+ .where(or(like(files.name, pattern), like(files.ocrText, pattern)))
+ .orderBy(desc(files.createdAt))
+ .limit(100)
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ mergeFromBackend(backendFiles: Array<{
+ id: string;
+ name: string;
+ mimeType: string;
+ size: number;
+ createdAt: string;
+ updatedAt?: string;
+ ocrText?: string;
+ tags?: Tag[];
+ isFolder: boolean;
+ parentFileId?: string;
+ thumbnailUrl?: string;
+ }>) {
+ const d = getDb();
+ const now = new Date().toISOString();
+
+ d.transaction(() => {
+ for (const bf of backendFiles) {
+ const existing = d.select().from(files).where(eq(files.backendId, bf.id)).get();
+
+ const source = existing && existing.localUri ? 'synced' : 'cloud';
+ const syncStatus = existing && existing.localUri
+ ? (existing.syncStatus === 'cloud' ? 'synced' : existing.syncStatus)
+ : 'cloud';
+
+ upsertRow({
+ id: bf.id,
+ backendId: bf.id,
+ name: bf.name,
+ mimeType: bf.mimeType,
+ size: bf.size,
+ source,
+ localUri: existing?.localUri ?? null,
+ syncStatus,
+ parentFileId: bf.parentFileId ?? null,
+ isFolder: bf.isFolder ? 1 : 0,
+ ocrText: bf.ocrText ?? null,
+ thumbnailUrl: bf.thumbnailUrl ?? null,
+ createdAt: bf.createdAt,
+ updatedAt: bf.updatedAt ?? now,
+ lastSyncedAt: now,
+ });
+
+ if (bf.tags) setTagsForFile(bf.id, bf.tags);
+ }
+ });
+ },
+
+ mergeFromDevice(deviceFiles: Array<{
+ id: string;
+ uri: string;
+ name: string;
+ mimeType: string;
+ size: number;
+ createdAt: string;
+ folderId?: string;
+ }>) {
+ const d = getDb();
+ const now = new Date().toISOString();
+
+ d.transaction(() => {
+ for (const df of deviceFiles) {
+ if (this.isDeleted(df.id)) continue;
+ const existing = d.select().from(files).where(eq(files.id, df.id)).get();
+ if (existing) continue;
+
+ upsertRow({
+ id: df.id,
+ backendId: null,
+ name: df.name,
+ mimeType: df.mimeType,
+ size: df.size,
+ source: 'local',
+ localUri: df.uri,
+ syncStatus: 'local',
+ parentFileId: df.folderId ?? null,
+ isFolder: 0,
+ ocrText: null,
+ thumbnailUrl: null,
+ createdAt: df.createdAt,
+ updatedAt: now,
+ lastSyncedAt: null,
+ });
+ }
+ });
+ },
+
+ updatePartial(id: string, updates: Partial) {
+ const d = getDb();
+ const setFields: Record = {};
+ if (updates.backendId !== undefined) setFields.backendId = updates.backendId;
+ if (updates.syncStatus !== undefined) setFields.syncStatus = updates.syncStatus;
+ if (updates.localUri !== undefined) setFields.localUri = updates.localUri;
+ if (updates.source !== undefined) setFields.source = updates.source;
+ if (updates.thumbnailUrl !== undefined) setFields.thumbnailUrl = updates.thumbnailUrl;
+ if (updates.ocrText !== undefined) setFields.ocrText = updates.ocrText;
+ if (updates.parentFileId !== undefined) setFields.parentFileId = updates.parentFileId;
+ if (updates.name !== undefined) setFields.name = updates.name;
+ setFields.updatedAt = new Date().toISOString();
+
+ d.update(files).set(setFields).where(eq(files.id, id)).run();
+ },
+
+ updateSyncStatus(id: string, syncStatus: string) {
+ this.updatePartial(id, { syncStatus });
+ },
+
+ markAsCloudOnly(id: string) {
+ this.updatePartial(id, { syncStatus: 'cloud', localUri: null, source: 'cloud' });
+ },
+
+ setThumbnailUrl(backendId: string, thumbnailUrl: string) {
+ const d = getDb();
+ d.update(files).set({ thumbnailUrl, updatedAt: new Date().toISOString() })
+ .where(eq(files.backendId, backendId)).run();
+ },
+
+ markDeleted(id: string) {
+ const d = getDb();
+ d.insert(deletedFiles).values({ id, deletedAt: new Date().toISOString() })
+ .onConflictDoUpdate({ target: deletedFiles.id, set: { deletedAt: new Date().toISOString() } })
+ .run();
+ },
+
+ isDeleted(id: string): boolean {
+ const d = getDb();
+ const row = d.select().from(deletedFiles).where(eq(deletedFiles.id, id)).get();
+ return !!row;
+ },
+
+ deleteById(id: string) {
+ const d = getDb();
+ this.markDeleted(id);
+ d.delete(files).where(eq(files.id, id)).run();
+ },
+
+ deleteByBackendId(backendId: string) {
+ const d = getDb();
+ const row = d.select().from(files).where(eq(files.backendId, backendId)).get();
+ if (row) this.markDeleted(row.id);
+ d.delete(files).where(eq(files.backendId, backendId)).run();
+ },
+
+ clear() {
+ const d = getDb();
+ d.delete(fileTags).run();
+ d.delete(files).run();
+ },
+
+ count(): number {
+ const d = getDb();
+ const row = d.select({ count: sql`count(*)` }).from(files).get();
+ return row?.count ?? 0;
+ },
+
+ getAllLocal(): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(or(eq(files.source, 'local'), eq(files.source, 'synced')))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ getAllSynced(): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(eq(files.source, 'synced'))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+
+ getPendingSync(): FileRecord[] {
+ const d = getDb();
+ const rows = d.select().from(files)
+ .where(and(eq(files.syncStatus, 'local'), sql`${files.backendId} IS NULL`))
+ .all();
+ return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
+ },
+};
diff --git a/mobile/services/fileStore/migrate.ts b/mobile/services/fileStore/migrate.ts
new file mode 100644
index 0000000..7aec87b
--- /dev/null
+++ b/mobile/services/fileStore/migrate.ts
@@ -0,0 +1,112 @@
+import { createMMKV } from 'react-native-mmkv';
+import { fileStore, FileRecord } from './index';
+import type { Tag, SyncStatus } from '../../types';
+
+const metadataStorage = createMMKV({ id: 'vaultdrop-metadata' });
+const localFilesStorage = createMMKV({ id: 'vaultdrop-local-files' });
+
+interface LegacyCachedFiles {
+ files: Array<{
+ id: string;
+ name: string;
+ mimeType: string;
+ size: number;
+ createdAt: string;
+ updatedAt: string;
+ ocrText?: string;
+ tags?: Tag[];
+ isFolder: boolean;
+ parentFileId?: string;
+ url?: string;
+ thumbnailUrl?: string;
+ }>;
+ page: number;
+ total: number;
+}
+
+interface LegacyRegistryBlob {
+ entries: Record;
+}
+
+export function migrateFromLegacy() {
+ const count = fileStore.count();
+ if (count > 0) return;
+
+ try {
+ const rawMetadata = metadataStorage.getString('backend_files_cache');
+ if (rawMetadata) {
+ const cached: LegacyCachedFiles = JSON.parse(rawMetadata);
+ for (const f of cached.files) {
+ fileStore.upsert({
+ id: f.id,
+ backendId: f.id,
+ name: f.name,
+ mimeType: f.mimeType,
+ size: f.size,
+ source: 'cloud',
+ localUri: null,
+ syncStatus: 'cloud',
+ parentFileId: f.parentFileId ?? null,
+ isFolder: f.isFolder ? 1 : 0,
+ ocrText: f.ocrText ?? null,
+ thumbnailUrl: f.thumbnailUrl ?? null,
+ createdAt: f.createdAt,
+ updatedAt: f.updatedAt,
+ lastSyncedAt: new Date().toISOString(),
+ tags: f.tags,
+ });
+ }
+ }
+ } catch {}
+
+ try {
+ const rawRegistry = localFilesStorage.getString('local_files_v2');
+ if (rawRegistry) {
+ const blob: LegacyRegistryBlob = JSON.parse(rawRegistry);
+ for (const entry of Object.values(blob.entries)) {
+ const existing = fileStore.getByBackendId(entry.backendFileId ?? '');
+ const source = entry.backendFileId
+ ? (entry.syncStatus === 'synced' ? 'synced' : 'cloud')
+ : 'local';
+
+ fileStore.upsert({
+ id: entry.id,
+ backendId: entry.backendFileId ?? null,
+ name: entry.name,
+ mimeType: entry.mimeType,
+ size: entry.size,
+ source,
+ localUri: entry.localUri,
+ syncStatus: entry.syncStatus,
+ parentFileId: entry.folderId ?? null,
+ isFolder: 0,
+ ocrText: null,
+ thumbnailUrl: null,
+ createdAt: entry.createdAt,
+ updatedAt: entry.createdAt,
+ lastSyncedAt: entry.backendFileId ? new Date().toISOString() : null,
+ tags: entry.tags,
+ });
+
+ if (existing && entry.localUri) {
+ fileStore.updatePartial(entry.id, {
+ localUri: entry.localUri,
+ source: 'synced',
+ syncStatus: 'synced',
+ });
+ }
+ }
+ }
+ } catch {}
+}
diff --git a/mobile/services/fileStore/schema.ts b/mobile/services/fileStore/schema.ts
new file mode 100644
index 0000000..967e33e
--- /dev/null
+++ b/mobile/services/fileStore/schema.ts
@@ -0,0 +1,47 @@
+import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
+
+export const files = sqliteTable(
+ 'files',
+ {
+ id: text('id').primaryKey(),
+ backendId: text('backend_id'),
+ name: text('name').notNull(),
+ mimeType: text('mime_type').notNull(),
+ size: integer('size').notNull(),
+ source: text('source').notNull().default('cloud'),
+ localUri: text('local_uri'),
+ syncStatus: text('sync_status').notNull().default('cloud'),
+ parentFileId: text('parent_file_id'),
+ isFolder: integer('is_folder').notNull().default(0),
+ ocrText: text('ocr_text'),
+ thumbnailUrl: text('thumbnail_url'),
+ createdAt: text('created_at').notNull(),
+ updatedAt: text('updated_at').notNull(),
+ lastSyncedAt: text('last_synced_at'),
+ },
+ (t) => [
+ index('idx_files_backend_id').on(t.backendId),
+ index('idx_files_parent_id').on(t.parentFileId),
+ index('idx_files_source').on(t.source),
+ index('idx_files_is_folder').on(t.isFolder),
+ index('idx_files_sync_status').on(t.syncStatus),
+ ],
+);
+
+export const fileTags = sqliteTable(
+ 'file_tags',
+ {
+ id: text('id').primaryKey(),
+ fileId: text('file_id')
+ .notNull()
+ .references(() => files.id, { onDelete: 'cascade' }),
+ tagName: text('tag_name').notNull(),
+ tagType: text('tag_type').notNull().default('none'),
+ },
+ (t) => [index('idx_file_tags_file_id').on(t.fileId)],
+);
+
+export const deletedFiles = sqliteTable('deleted_files', {
+ id: text('id').primaryKey(),
+ deletedAt: text('deleted_at').notNull(),
+});
diff --git a/mobile/services/localFileRegistry.ts b/mobile/services/localFileRegistry.ts
deleted file mode 100644
index d6f0e1f..0000000
--- a/mobile/services/localFileRegistry.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-import { LocalFileEntry, SyncStatus } from '../types';
-
-const storage = createMMKV({ id: 'vaultdrop-local-files' });
-
-const BLOB_KEY = 'local_files_v2';
-const LEGACY_INDEX_KEY = 'local_files_index';
-
-interface RegistryBlob {
- entries: Record;
- backendIndex: Record;
-}
-
-let memoryCache: RegistryBlob | null = null;
-
-function loadBlob(): RegistryBlob {
- if (memoryCache) return memoryCache;
-
- const raw = storage.getString(BLOB_KEY);
- if (raw) {
- memoryCache = JSON.parse(raw) as RegistryBlob;
- return memoryCache;
- }
-
- memoryCache = migrateFromLegacy();
- saveBlob(memoryCache);
- return memoryCache;
-}
-
-function saveBlob(blob: RegistryBlob) {
- memoryCache = blob;
- storage.set(BLOB_KEY, JSON.stringify(blob));
-}
-
-function migrateFromLegacy(): RegistryBlob {
- const blob: RegistryBlob = { entries: {}, backendIndex: {} };
-
- const rawIndex = storage.getString(LEGACY_INDEX_KEY);
- if (!rawIndex) return blob;
-
- const ids: string[] = JSON.parse(rawIndex);
- for (const id of ids) {
- const raw = storage.getString(`local_file_${id}`);
- if (!raw) continue;
- const entry: LocalFileEntry = JSON.parse(raw);
- blob.entries[entry.id] = entry;
- if (entry.backendFileId) {
- blob.backendIndex[entry.backendFileId] = entry.id;
- }
- }
-
- storage.remove(LEGACY_INDEX_KEY);
- for (const id of ids) {
- storage.remove(`local_file_${id}`);
- }
-
- return blob;
-}
-
-export const localFileRegistry = {
- register(entry: LocalFileEntry) {
- const blob = loadBlob();
- blob.entries[entry.id] = entry;
- if (entry.backendFileId) {
- blob.backendIndex[entry.backendFileId] = entry.id;
- }
- saveBlob(blob);
- },
-
- registerBatch(entries: LocalFileEntry[]) {
- if (entries.length === 0) return;
- const blob = loadBlob();
- for (const entry of entries) {
- blob.entries[entry.id] = entry;
- if (entry.backendFileId) {
- blob.backendIndex[entry.backendFileId] = entry.id;
- }
- }
- saveBlob(blob);
- },
-
- get(id: string): LocalFileEntry | undefined {
- return loadBlob().entries[id];
- },
-
- getByBackendId(backendId: string): LocalFileEntry | undefined {
- const blob = loadBlob();
- const entryId = blob.backendIndex[backendId];
- if (!entryId) return undefined;
- return blob.entries[entryId];
- },
-
- getAll(): LocalFileEntry[] {
- const blob = loadBlob();
- return Object.values(blob.entries);
- },
-
- update(id: string, updates: Partial) {
- const blob = loadBlob();
- const existing = blob.entries[id];
- if (!existing) return;
-
- if (existing.backendFileId && updates.backendFileId === undefined && updates.syncStatus === 'cloud') {
- delete blob.backendIndex[existing.backendFileId];
- }
-
- const updated = { ...existing, ...updates };
- blob.entries[id] = updated;
- if (updated.backendFileId) {
- blob.backendIndex[updated.backendFileId] = id;
- }
- saveBlob(blob);
- },
-
- updateSyncStatus(id: string, syncStatus: SyncStatus) {
- this.update(id, { syncStatus });
- },
-
- markAsCloudOnly(id: string) {
- this.update(id, { syncStatus: 'cloud', localUri: '' });
- },
-
- remove(id: string) {
- const blob = loadBlob();
- const entry = blob.entries[id];
- if (entry?.backendFileId) {
- delete blob.backendIndex[entry.backendFileId];
- }
- delete blob.entries[id];
- saveBlob(blob);
- },
-
- removeByBackendId(backendId: string) {
- const blob = loadBlob();
- const entryId = blob.backendIndex[backendId];
- if (entryId) {
- delete blob.entries[entryId];
- delete blob.backendIndex[backendId];
- saveBlob(blob);
- }
- },
-
- count(): number {
- return Object.keys(loadBlob().entries).length;
- },
-};
diff --git a/mobile/services/metadataCache.ts b/mobile/services/metadataCache.ts
deleted file mode 100644
index c49a7e6..0000000
--- a/mobile/services/metadataCache.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-import { FileItem } from '../types';
-
-const storage = createMMKV({ id: 'vaultdrop-metadata' });
-
-const FILES_KEY = 'backend_files_cache';
-const UPDATED_AT_KEY = 'cache_updated_at';
-const STALE_MS = 5 * 60 * 1000;
-
-interface CachedFiles {
- files: FileItem[];
- page: number;
- total: number;
-}
-
-export const metadataCache = {
- getFiles(): CachedFiles | null {
- const raw = storage.getString(FILES_KEY);
- if (!raw) return null;
- try {
- return JSON.parse(raw) as CachedFiles;
- } catch {
- return null;
- }
- },
-
- setFiles(files: FileItem[], page: number, total: number) {
- const data: CachedFiles = { files, page, total };
- storage.set(FILES_KEY, JSON.stringify(data));
- storage.set(UPDATED_AT_KEY, Date.now());
- },
-
- isStale(): boolean {
- const raw = storage.getString(UPDATED_AT_KEY);
- if (!raw) return true;
- const updatedAt = Number(raw);
- return Date.now() - updatedAt > STALE_MS;
- },
-
- clear() {
- storage.remove(FILES_KEY);
- storage.remove(UPDATED_AT_KEY);
- },
-};
diff --git a/mobile/services/mmkvPersister.ts b/mobile/services/mmkvPersister.ts
new file mode 100644
index 0000000..f552603
--- /dev/null
+++ b/mobile/services/mmkvPersister.ts
@@ -0,0 +1,20 @@
+import { createMMKV } from 'react-native-mmkv';
+import { PersistedClient, Persister } from '@tanstack/react-query-persist-client';
+
+const storage = createMMKV({ id: 'vaultdrop-query-cache' });
+
+export function createMMKVPersister(): Persister {
+ return {
+ persistClient: async (client: PersistedClient) => {
+ storage.set('query-cache', JSON.stringify(client));
+ },
+ restoreClient: async () => {
+ const raw = storage.getString('query-cache');
+ if (!raw) return undefined;
+ return JSON.parse(raw) as PersistedClient;
+ },
+ removeClient: async () => {
+ storage.remove('query-cache');
+ },
+ };
+}
diff --git a/mobile/services/thumbnailCache.ts b/mobile/services/thumbnailCache.ts
deleted file mode 100644
index 37347c4..0000000
--- a/mobile/services/thumbnailCache.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-
-const storage = createMMKV({ id: 'vaultdrop-thumbnails' });
-
-const BLOB_KEY = 'thumbnail_urls';
-const STALE_MS = 50 * 60 * 1000;
-
-interface ThumbnailCacheEntry {
- url: string;
- expiresAt: number;
-}
-
-let memoryCache: Record | null = null;
-
-function loadCache(): Record {
- if (memoryCache) return memoryCache;
- const raw = storage.getString(BLOB_KEY);
- memoryCache = raw ? JSON.parse(raw) : {};
- return memoryCache!;
-}
-
-function saveCache(cache: Record) {
- memoryCache = cache;
- storage.set(BLOB_KEY, JSON.stringify(cache));
-}
-
-export const thumbnailCache = {
- get(fileId: string): string | null {
- const cache = loadCache();
- const entry = cache[fileId];
- if (!entry) return null;
- if (Date.now() > entry.expiresAt) {
- delete cache[fileId];
- saveCache(cache);
- return null;
- }
- return entry.url;
- },
-
- set(fileId: string, url: string, expiresAt: number) {
- const cache = loadCache();
- cache[fileId] = { url, expiresAt };
- saveCache(cache);
- },
-
- setBatch(entries: Array<{ fileId: string; url: string; expiresAt: number }>) {
- if (entries.length === 0) return;
- const cache = loadCache();
- for (const e of entries) {
- cache[e.fileId] = { url: e.url, expiresAt: e.expiresAt };
- }
- saveCache(cache);
- },
-
- remove(fileId: string) {
- const cache = loadCache();
- delete cache[fileId];
- saveCache(cache);
- },
-
- clear() {
- memoryCache = {};
- storage.remove(BLOB_KEY);
- },
-};
diff --git a/mobile/types/index.ts b/mobile/types/index.ts
index 2f596db..1cf531e 100644
--- a/mobile/types/index.ts
+++ b/mobile/types/index.ts
@@ -8,13 +8,17 @@ export interface Thumbnail {
mimeType: string;
}
-export interface FileItem {
+export interface UnifiedFileItem {
id: string;
+ backendFileId?: string;
name: string;
mimeType: string;
size: number;
createdAt: string;
- updatedAt: string;
+ updatedAt?: string;
+ source: 'cloud' | 'local' | 'synced';
+ syncStatus: SyncStatus;
+ localUri?: string;
ocrText?: string;
tags: Tag[];
isFolder: boolean;
@@ -22,9 +26,12 @@ export interface FileItem {
url?: string;
thumbnailUrl?: string;
thumbnails?: Thumbnail[];
+ isDeviceFile?: boolean;
}
-export function isFolder(file: FileItem | { isFolder: boolean }): boolean {
+export type FileItem = UnifiedFileItem;
+
+export function isFolder(file: UnifiedFileItem | { isFolder: boolean }): boolean {
return file.isFolder;
}
@@ -118,16 +125,3 @@ export interface RefreshResponse {
}
export type SyncStatus = 'local' | 'syncing' | 'synced' | 'cloud' | 'conflict';
-
-export interface LocalFileEntry {
- id: string;
- backendFileId?: string;
- localUri: string;
- name: string;
- mimeType: string;
- size: number;
- syncStatus: SyncStatus;
- createdAt: string;
- tags?: Tag[];
- folderId?: string;
-}