296 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 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`)
 | |
| 
 | |
| ```typescript
 | |
| 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.
 | |
| 
 | |
| ```typescript
 | |
| 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é.
 | |
| 
 | |
| ```html
 | |
| <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`
 | |
| 
 | |
| ```typescript
 | |
| // 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 }));
 | |
| };
 | |
| ```
 | |
| 
 | |
| ```typescript
 | |
| // 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:
 | |
| 
 | |
| ```javascript
 | |
| // 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  
 |