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
|