Skip to content

feat: mano offline#1523

Draft
arnaudambro wants to merge 18 commits intomainfrom
feat--mano-offline
Draft

feat: mano offline#1523
arnaudambro wants to merge 18 commits intomainfrom
feat--mano-offline

Conversation

@arnaudambro
Copy link
Copy Markdown
Collaborator

@arnaudambro arnaudambro commented Mar 24, 2026

Philosophie

quelles philosophies ici ?
il existe plusieurs stratégies pour le mode hors-ligne

  1. on se base sur la connexion réelle du téléphone, et lorsqu'il n'y a plus de réseau on bascule en mode hors-ligne (comme l'offre le web à travers l'interception des requêtes par des service workers). je n'ai pas retenu ce fonctionnement : détecter le réseau faible / pas de réseau n'est pas très fiable sur mobile, et surtout c'est rare qu'il n'y ait jamais de réseau, il y a souvent un réseau très très faible, et l'expérience utilisateur dans ce cas est très dégradée
  2. on fonctionne en local-first avec queue de synchronisation, et quand il n'y a pas de réseau la queue augmente jusqu'à ce que le réseau revienne pour vider la queue. je n'ai pas retenu ce fonctionnement : détecter le réseau faible / pas de réseau n'est pas très fiable sur mobile. aussi, le local first aurait demandé une grosse refonte du fonctionnement de l'app - on peut tout aussi bien le faire ensuite (il faudrait de toutes façons, avec refonte du loader pour le faire fonctionner de manière robuste comme sur le dashboard)
  3. l'utilisateur toggle le mode hors-ligne, de sorte que la logique hors-ligne est activée manuellement aussi dans le code - simple à maintenir, simple à comprendre par l'utilisateur. inconvénient : ce n'est pas automatique. mais à y réfléchir, c'est plus safe.

je propose donc qu'on parte avec cette option 3 pour le moment, et quand/si on refacto le loader, on peut switcher sur une autre expérience tout en conservant les utils et logiques créées ici (interception, queue, sync)

NOTE: tout est questionnable ici : autant l'interception que le queue et le sync. objectif : le moins de charge cognitive possible !

Mise en place

L'utilisateur active/désactive manuellement le mode hors-ligne via un toggle dans le menu en bas à droite. Pas de détection réseau automatique : c'est plus fiable, plus simple à comprendre, et plus simple à maintenir.

Interception des requêtes

Lorsque le mode hors-ligne est activé, la classe api intercepte les POST/PUT/DELETE métier - les autres requêtes, peu nombreuses (login etc.), ont un champ offlineEnabled=false. Au lieu d'envoyer la requête au serveur, elle la met en queue (persistée dans MMKV) et retourne une réponse optimiste immédiate pour que l'UI se mette à jour instantanément.

Pour les POST, un UUID est généré côté client, ce qui permet de créer dans la foulée des entités liées
(actions, commentaires, etc.).

Fusion des requêtes

Si plusieurs modifications sont faites sur la même entité, elles sont fusionnées intelligemment :

  • PUT après POST → reste un seul POST enrichi (le serveur ne connaît pas encore l'entité)
  • PUT après PUT → les champs sont fusionnés, le updatedAt d'origine est conservé (pour la détection de
    conflits)
  • DELETE après POST → annulation pure, l'item est retiré de la queue (l'entité n'a jamais existé côté
    serveur)
  • DELETE après PUT → remplacé par un DELETE, le updatedAt d'origine est conservé

Chiffrement

Les données texte sont stockées déchiffrées dans la queue — c'est ce qui permet la fusion et la détection de conflits champ par champ. Elles sont re-chiffrées au moment du sync. Ça ne pose pas plus de problème que ce qu'on a actuellement : aujourd'hui le cache enregistré dans MMKV est aussi déchiffré.

Les fichiers sont chiffrés avant mise en queue — pas besoin de les déchiffrer pour les fusionner, et ils sont déchiffrés à la volée quand on clique dessus de toutes façons, donc autant les garder chiffrés, c'est une logique plus simple globalement, moins de code.

Synchronisation

Quand le mode hors-ligne est désactivé :

  1. Pull sync — récupération de l'état frais du serveur
  2. Détection de conflits — pour chaque PUT/DELETE, comparaison du updatedAt capturé au moment de la mise en queue avec la version serveur actuelle
  3. Exécution ou résolution — s'il n'y a pas de conflit, la requête est rejouée normalement via la classe api. S'il y a conflit, l'utilisateur voit un écran de résolution avec comparaison champ par champ et choisit "garder ma version" ou "garder la version serveur"
  4. Pull sync final — pour confirmer l'état final

Nouveauté

Il y a des tests !

  • tests unitaires pour le offline
  • tests avec React Native Testing Library pour tester que les bons inputs arrivent dans l'API.put (notamment avoir les updatedAt critiques au bon fonctionnement du mode hors-ligne)

@arnaudambro arnaudambro marked this pull request as draft March 24, 2026 12:40
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pourquoi ce fichier : parce qu'on a sinon des cycles d'import: api > store > sync > api, et ça casse un peu tout

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pourquoi ce fichier : idem, parce qu'on a sinon des cycles d'import

}

// TODO: refactor the loader to simply await it
async function pullSync(): Promise<void> {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hésitation ici : est-ce que je fais une PR séparée pour faire ce refacto du loader ? AVANT cette PR ? ou bien on laisse ça comme ça pour le moment ?

<ScrollContainer noRadius>
<DocumentsManager
defaultParent="root"
uploadPath={`/person/${person?._id}/document`}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(pour faire taire typescript, désolé)

return this.execute({ method, path, body });
};

_extractEntityType = (path) => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: regarder si ça marche bien avec tout

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant