ObsiViewer/docs/EXCALIDRAW_SAVE_IMPROVEMENTS.md

10 KiB

Améliorations du Système de Sauvegarde Excalidraw

Modifications Appliquées

1. Hash Stable et Déterministe

Fichier: src/app/features/drawings/drawings-editor.component.ts

Problème résolu: Hash instable causant des faux positifs/négatifs pour le dirty state.

Solution:

  • Tri stable des éléments par id pour éviter les changements de hash dus à l'ordre
  • Tri stable des clés de files pour éviter les changements de hash dus à l'ordre des propriétés
  • Suppression des propriétés volatiles (version, versionNonce, updated)
private hashScene(scene: ExcalidrawScene | null): string {
  try {
    if (!scene) return '';
    
    // Normalize elements: remove volatile properties
    const normEls = Array.isArray(scene.elements) ? scene.elements.map((el: any) => {
      const { version, versionNonce, updated, ...rest } = el || {};
      return rest;
    }) : [];

    // Stable sort of elements by id
    const sortedEls = normEls.slice().sort((a: any, b: any) => 
      (a?.id || '').localeCompare(b?.id || '')
    );
    
    // Stable sort of files keys
    const filesObj = scene.files && typeof scene.files === 'object' ? scene.files : {};
    const sortedFilesKeys = Object.keys(filesObj).sort();
    const sortedFiles: Record<string, any> = {};
    for (const k of sortedFilesKeys) sortedFiles[k] = filesObj[k];

    const stable = { elements: sortedEls, files: sortedFiles };
    return btoa(unescape(encodeURIComponent(JSON.stringify(stable))));
  } catch (error) {
    console.error('Error hashing scene:', error);
    return '';
  }
}

2. Prévention des Sauvegardes Concurrentes

Fichier: src/app/features/drawings/drawings-editor.component.ts

Problème résolu: Plusieurs PUT peuvent être lancés simultanément, causant des conflits.

Solution: Ajout d'un filtre pour ignorer les autosaves si une sauvegarde est déjà en cours.

this.saveSub = sceneChange$.pipe(
  distinctUntilChanged((prev, curr) => prev.hash === curr.hash),
  debounceTime(2000),
  filter(({ hash }) => this.lastSavedHash !== hash),
  filter(() => !this.isSaving()),  // ⬅️ NOUVEAU: Empêche les sauvegardes concurrentes
  tap(({ hash }) => {
    console.log('💾 Autosaving... hash:', hash.substring(0, 10));
    this.isSaving.set(true);
    this.error.set(null);
  }),
  switchMap(({ scene, hash }) => this.files.put(this.path, scene).pipe(...))
).subscribe();

3. Suppression du Mode readOnly Pendant la Sauvegarde

Fichier: src/app/features/drawings/drawings-editor.component.html

Problème résolu: Le passage en readOnly pendant la sauvegarde peut perturber les événements onChange et bloquer des micro-changements.

Solution: Suppression de [readOnly]="isSaving()", conservation uniquement de l'indicateur visuel d'opacité.

<excalidraw-editor
  #editorEl
  [initialData]="scene() || { elements: [], appState: { viewBackgroundColor: '#1e1e1e' } }"
  [theme]="themeName()"
  [lang]="'fr'"
  (ready)="console.log('READY', $event); onExcalidrawReady()"
  style="display:block; height:100%; width:100%"
></excalidraw-editor>

4. Logs de Diagnostic Améliorés

Fichiers:

  • web-components/excalidraw/ExcalidrawElement.tsx
  • web-components/excalidraw/define.ts
  • src/app/features/drawings/drawings-editor.component.html

Ajouts:

  • Log visible console.log au lieu de console.debug pour scene-change
  • Log du ready event dans le web component
  • Log du ready event dans le template Angular
  • Tous les événements avec bubbles: true, composed: true
// ExcalidrawElement.tsx
const onChange = (elements: any[], appState: Partial<AppState>, files: any) => {
  if (!host) return;
  const detail: SceneChangeDetail = { elements, appState, files, source: 'user' };
  console.log('[excalidraw-editor] 📝 SCENE-CHANGE dispatched', { 
    elCount: Array.isArray(elements) ? elements.length : 'n/a' 
  });
  host.dispatchEvent(new CustomEvent('scene-change', { detail, bubbles: true, composed: true }));
};
// define.ts
const onReady = () => {
  console.log('[excalidraw-editor] 🎨 READY event dispatched', { 
    apiAvailable: !!(this as any).__excalidrawAPI 
  });
  this.dispatchEvent(new CustomEvent('ready', { 
    detail: { apiAvailable: !!(this as any).__excalidrawAPI },
    bubbles: true,
    composed: true
  }));
};

Check-list de Test

1. Vérifier les Événements de Base

Ouvrir la console DevTools et observer:

// Test manuel dans la console
document.querySelector('excalidraw-editor')
  ?.addEventListener('scene-change', e => console.log('✅ SCENE-CHANGE reçu', e?.detail));

Attendu:

  • [excalidraw-editor] 🎨 READY event dispatched au chargement
  • READY { detail: { apiAvailable: true } } dans le template Angular
  • 🎨 Excalidraw Ready - Binding listeners dans le composant
  • 🔗 Binding Excalidraw host listeners dans le composant
  • [excalidraw-editor] 📝 SCENE-CHANGE dispatched à chaque modification
  • ✏️ Dirty flagged (event) dans Angular après chaque modification

2. Tester le Dirty State

  1. Ouvrir un fichier .excalidraw.md
  2. Vérifier: indicateur Gris "Sauvegardé"
  3. Ajouter un rectangle
  4. Vérifier: indicateur 🔴 Rouge "Non sauvegardé" immédiatement
  5. Attendre 2 secondes
  6. Vérifier console: 💾 Autosaving...
  7. Vérifier console: ✅ Autosave successful
  8. Vérifier: indicateur Gris "Sauvegardé"

3. Tester la Sauvegarde Manuelle

  1. Modifier le dessin
  2. Appuyer Ctrl+S (ou cliquer sur l'indicateur)
  3. Vérifier console: 💾 Manual save triggered
  4. Vérifier console: 📤 Sending save request...
  5. Vérifier console: ✅ Manual save successful
  6. Vérifier toast: "Sauvegarde réussie"
  7. Vérifier Network: PUT /api/files?path=...
  8. Vérifier: indicateur Gris "Sauvegardé"

4. Tester la Stabilité du Hash

  1. Ouvrir un fichier
  2. Ajouter un élément → attendre autosave
  3. Déplacer légèrement l'élément → attendre autosave
  4. Vérifier console: les hash doivent être différents
  5. Ne rien toucher pendant 5 secondes
  6. Vérifier: pas de nouveaux autosaves (hash stable)

5. Tester les Sauvegardes Concurrentes

  1. Modifier rapidement plusieurs éléments
  2. Immédiatement appuyer Ctrl+S plusieurs fois
  3. Vérifier console: un seul 💾 Autosaving... à la fois
  4. Vérifier Network: pas de requêtes PUT simultanées

6. Tester les Conflits (ETag)

  1. Ouvrir un fichier
  2. Modifier externellement le fichier (autre éditeur)
  3. Modifier dans Excalidraw
  4. Attendre l'autosave
  5. Vérifier: bannière de conflit apparaît
  6. Tester "Recharger depuis le disque" ou "Écraser"

Comportement Attendu

Séquence de Chargement

1. GET /api/files?path=test.excalidraw.md
2. [excalidraw-editor] 🎨 READY event dispatched
3. READY { detail: { apiAvailable: true } }
4. 🎨 Excalidraw Ready - Binding listeners
5. 🔗 Binding Excalidraw host listeners
6. ✓ Dirty check and Autosave subscriptions active
7. Indicateur: ⚫ Gris "Sauvegardé"

Séquence de Modification

1. Utilisateur dessine un rectangle
2. [excalidraw-editor] 📝 SCENE-CHANGE dispatched { elCount: 1 }
3. ✏️ Dirty flagged (event) { lastSaved: 'abc123...', current: 'def456...' }
4. Indicateur: 🔴 Rouge "Non sauvegardé"
5. (attente 2s)
6. 💾 Autosaving... hash: def456...
7. PUT /api/files?path=... (avec If-Match: "...")
8. ✅ Autosave successful { newHash: 'def456...' }
9. Indicateur: ⚫ Gris "Sauvegardé"

Séquence de Sauvegarde Manuelle

1. Utilisateur appuie Ctrl+S
2. 💾 Manual save triggered
3. 📤 Sending save request...
4. 🧩 Snapshot { elements: 3, hasFiles: true }
5. PUT /api/files?path=...
6. 📥 Save response { rev: '...' }
7. 🔁 Verify after save { ok: true, ... }
8. ✅ Manual save successful
9. Toast: "Sauvegarde réussie"
10. Indicateur: ⚫ Gris "Sauvegardé"

Troubleshooting

Pas de logs [excalidraw-editor]

→ Le web component ne se charge pas. Vérifier ngOnInit() et l'import du custom element.

READY n'apparaît jamais

→ L'API Excalidraw ne s'initialise pas. Vérifier la console pour des erreurs React.

SCENE-CHANGE n'apparaît jamais

→ Le onChange n'est pas appelé. Vérifier que le composant React reçoit bien __host.

✏️ Dirty flagged n'apparaît jamais

→ Le binding n'a pas eu lieu. Vérifier que onExcalidrawReady() est appelé.

Hash change en permanence (autosave en boucle)

→ Le hash n'est pas stable. Vérifier que la nouvelle version de hashScene() est bien appliquée.

Indicateur reste rouge après autosave

→ Vérifier que this.dirty.set(false) est bien appelé dans le tap() après succès.

Conflits 409 en permanence

→ Vérifier que le serveur renvoie bien un ETag dans les réponses GET et PUT.

Fichiers Modifiés

  1. src/app/features/drawings/drawings-editor.component.ts

    • hashScene(): tri stable des éléments et files
    • bindEditorHostListeners(): ajout du filtre !isSaving()
  2. src/app/features/drawings/drawings-editor.component.html

    • Suppression de [readOnly]="isSaving()"
    • Ajout de log du ready event
  3. web-components/excalidraw/ExcalidrawElement.tsx

    • Log visible console.log pour scene-change
  4. web-components/excalidraw/define.ts

    • Log visible console.log pour ready
    • Ajout de bubbles: true, composed: true au ready event

Prochaines Étapes

Une fois les tests validés:

  1. Retirer les logs de debug (optionnel, utiles pour le monitoring)
  2. Tester avec des fichiers volumineux (100+ éléments)
  3. Tester sur mobile (touch events)
  4. Ajouter des tests E2E pour la sauvegarde automatique
  5. Documenter le comportement ETag côté backend

Résumé

Hash stable et déterministe (tri des éléments et files)
Prévention des sauvegardes concurrentes
Suppression du readOnly pendant la sauvegarde
Logs de diagnostic améliorés
Événements avec bubbles et composed
Check-list de test complète