Files
siop2/apps/mobile/app/_layout.tsx
pr-daaif c8b3c1769a feat(r4.3): file d'écriture, verrou optimiste D2, Synchro & conflits
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur
transition/coche/bilan → 409 « Conflit de version » contextualisé (qui,
quand) ; toute écriture secondaire (coche, bilan, commentaire, conso,
MO) fait avancer la version — sans version fournie, le web est
inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où
sur l'appareil, purge complète à la déconnexion — correctif réel : la
file et le cache persisté survivaient au logout).

Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès →
propagation de la version fraîche aux saisies restantes du même OT
(nos écritures ne se conflictent pas entre elles, un écart étranger
reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file
s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour /
abandonner). Transitions, coches, bilan et photos (D5, compressées
~1600 px) passent par la file avec patch optimiste du cache ; écran
Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels
(le bilan hors-ligne a ses vocabulaires).

Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne →
3 en file → modification concurrente de Salma → conflit tranché →
serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:50:02 +01:00

81 lines
2.5 KiB
TypeScript

import {
Manrope_400Regular,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
useFonts,
} from '@expo-google-fonts/manrope';
import NetInfo from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { onlineManager, QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react';
import { HorsLigneProvider } from '@/auth/session';
import { chargerFile } from '@/file/store';
import { rejouer } from '@/file/synchro';
import { useTokens } from '@/theme/tokens';
/** Racine de l'app (D1 validée) : le cache TanStack est PERSISTÉ dans
* AsyncStorage — les OT lus restent lisibles hors-ligne, y compris après
* redémarrage. NetInfo pilote onlineManager (pas de retry dans le vide)
* et le drapeau hors-ligne des écrans. */
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
gcTime: 7 * 24 * 3600_000, // une semaine de lecture hors-ligne
retry: 1,
},
},
});
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'siop.cache',
});
export default function RacineApp() {
const t = useTokens();
const [horsLigne, setHorsLigne] = useState(false);
const [polices] = useFonts({
Manrope_400Regular,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
});
useEffect(() => {
// D1 : la file survit au redémarrage — on la recharge avant tout.
void chargerFile().then(() => rejouer(queryClient));
return NetInfo.addEventListener((etat) => {
const enLigne = !!etat.isConnected;
onlineManager.setOnline(enLigne);
setHorsLigne(!enLigne);
if (enLigne) void rejouer(queryClient); // le retour du réseau vide la file
});
}, []);
if (!polices) return null;
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister, maxAge: 7 * 24 * 3600_000 }}
>
<HorsLigneProvider valeur={horsLigne}>
<StatusBar style="auto" />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: t.fond },
}}
/>
</HorsLigneProvider>
</PersistQueryClientProvider>
);
}