Cours JavaScript et TypeScript · Module 4: JavaScript asynchrone
Gestion des requêtes HTTP - patterns pratiques et techniques avancées
Dans cette leçon9
Dans l'exercice précédent, nous avons couvert les bases de la Fetch API. Élargissons maintenant nos connaissances avec des techniques plus avancées et des patterns pratiques pour construire un système de communication robuste pour notre Parc Jurassique.
Gestion complète des erreurs
Dans un environnement aussi critique que le Parc Jurassique, où une panne système pourrait conduire à des situations dangereuses, une bonne gestion des erreurs dans la communication HTTP est extrêmement importante. Créons un mécanisme de gestion des erreurs plus avancé :
1async function safeRequest(url, options = {}) {
2 const controller = new AbortController();
3 const { signal } = controller;
4
5 // Définir un timeout (10 secondes)
6 const timeoutId = setTimeout(() => controller.abort(), 10000);
7
8 try {
9 const response = await fetch(url, { ...options, signal });
10 clearTimeout(timeoutId);
11
12 // Catégories d'erreurs HTTP
13 if (response.status >= 500) {
14 throw new Error('Erreur serveur, probablement un problème temporaire.');
15 } else if (response.status === 404) {
16 throw new Error('Ressource introuvable.');
17 } else if (response.status === 403) {
18 throw new Error('Aucune permission pour accéder à cette ressource.');
19 } else if (response.status === 401) {
20 throw new Error('Authentification requise. Merci de te reconnecter.');
21 } else if (!response.ok) {
22 throw new Error(`Erreur HTTP inattendue : ${response.status}`);
23 }
24
25 // Gérer différents types de réponse
26 const contentType = response.headers.get('content-type');
27 if (contentType && contentType.includes('application/json')) {
28 return await response.json();
29 } else if (contentType && contentType.includes('text/')) {
30 return await response.text();
31 } else {
32 return await response.blob();
33 }
34 } catch (error) {
35 clearTimeout(timeoutId);
36
37 // Gérer différents types d'erreurs
38 if (error.name === 'AbortError') {
39 throw new Error('Requête annulée - délai dépassé (10s).');
40 } else if (!navigator.onLine) {
41 throw new Error('Pas de connexion internet. Vérifie ta connexion réseau.');
42 } else {
43 console.error('Détails de l\'erreur :', error);
44 throw new Error(`Erreur de communication avec le serveur : ${error.message}`);
45 }
46 }
47}Pattern Retry - nouvelles tentatives automatiques des requêtes
Sur le réseau du Parc Jurassique, des problèmes de connectivité temporaires peuvent survenir, surtout dans les zones reculées de l'île. Implémentons un mécanisme de nouvelles tentatives automatiques pour les requêtes échouées :
1/**
2 * Exécute une requête HTTP avec des nouvelles tentatives automatiques
3 * @param {Function} requestFn - Fonction effectuant la requête, retournant une Promise
4 * @param {Object} options - Options de configuration
5 * @returns {Promise} - Résultat de la requête ou dernière erreur après épuisement des tentatives
6 */
7async function withRetry(requestFn, options = {}) {
8 const {
9 maxAttempts = 3,
10 initialDelay = 1000,
11 delayFactor = 2,
12 maxDelay = 10000,
13 timeoutMs = 10000,
14 shouldRetry = (error) => {
15 // Par défaut, ne réessayer que pour les erreurs serveur (5xx) et les problèmes réseau
16 return (
17 error.message.includes('Erreur serveur') ||
18 error.message.includes('Pas de connexion') ||
19 error.name === 'AbortError'
20 );
21 }
22 } = options;
23
24 let lastError;
25 let attemptNum = 0;
26
27 while (attemptNum <= maxAttempts) {
28 try {
29 // Pour chaque tentative, créer un nouveau contrôleur d'annulation
30 const controller = new AbortController();
31 const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
32
33 // Si ce n'est pas la première tentative, attendre avant de réessayer
34 if (attemptNum > 0) {
35 const delayTime = Math.min(
36 initialDelay * Math.pow(delayFactor, attemptNum - 1),
37 maxDelay
38 );
39 console.log(`Tentative ${attemptNum}/${maxAttempts} échouée. Nouvelle tentative dans ${delayTime}ms...`);
40 await new Promise(resolve => setTimeout(resolve, delayTime));
41 }
42
43 // Exécuter la requête
44 const result = await requestFn(controller.signal);
45 clearTimeout(timeoutId);
46 return result;
47 } catch (error) {
48 clearTimeout(timeoutId);
49 lastError = error;
50
51 // Vérifier s'il faut réessayer
52 if (attemptNum === maxAttempts || !shouldRetry(error)) {
53 break;
54 }
55
56 attemptNum++;
57 }
58 }
59
60 // Lancer la dernière erreur après épuisement de toutes les tentatives
61 throw new Error(`Échec après ${maxAttempts} tentatives : ${lastError.message}`);
62}
63
64// Exemple d'utilisation
65async function fetchDinosaurInfo(id) {
66 return withRetry(
67 async (signal) => {
68 const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, { signal });
69
70 if (!response.ok) {
71 throw new Error(`Erreur HTTP : ${response.status}`);
72 }
73
74 return response.json();
75 },
76 {
77 maxAttempts: 5,
78 initialDelay: 500,
79 shouldRetry: (error) => error.message.includes('Erreur HTTP : 5')
80 }
81 );
82}File d'attente de requêtes et limitation des connexions simultanées
Dans les situations où nous devons effectuer de nombreuses requêtes HTTP mais voulons limiter le nombre de connexions simultanées pour ne pas surcharger le réseau ou le serveur, nous pouvons implémenter une file d'attente de requêtes :
1class TaskQueue {
2 constructor(maxConcurrency = 3) {
3 this.maxConcurrency = maxConcurrency;
4 this.activeTasks = 0;
5 this.queue = [];
6 }
7
8 async add(task) {
9 // S'il y a de la place pour plus de tâches parallèles, exécuter immédiatement
10 if (this.activeTasks < this.maxConcurrency) {
11 return this.execute(task);
12 }
13
14 // Sinon, ajouter à la file d'attente
15 return new Promise((resolve, reject) => {
16 this.queue.push({ task, resolve, reject });
17 });
18 }
19
20 async execute(task) {
21 this.activeTasks++;
22
23 try {
24 // Exécuter la tâche
25 const result = await task();
26 return result;
27 } catch (error) {
28 throw error;
29 } finally {
30 this.activeTasks--;
31 this.processNext();
32 }
33 }
34
35 processNext() {
36 if (this.queue.length === 0 || this.activeTasks >= this.maxConcurrency) {
37 return;
38 }
39
40 // Récupérer la prochaine tâche de la file
41 const { task, resolve, reject } = this.queue.shift();
42
43 // Exécuter la tâche et transmettre le résultat à la Promise en attente
44 this.execute(task)
45 .then(resolve)
46 .catch(reject);
47 }
48}
49
50// Exemple d'utilisation de la file pour surveiller de nombreux dinosaures
51async function monitorAllDinosaurs(idList) {
52 const queue = new TaskQueue(5); // Max 5 requêtes simultanées
53 const results = [];
54 const errors = [];
55
56 // Fonctions utilitaires pour suivre la progression
57 const totalTasks = idList.length;
58 let completedTasks = 0;
59
60 function updateProgress() {
61 completedTasks++;
62 const percent = Math.floor((completedTasks / totalTasks) * 100);
63 console.log(`Progression de la surveillance : ${percent}% (${completedTasks}/${totalTasks})`);
64 }
65
66 // Créer un tableau de Promises pour toutes les tâches
67 const promises = idList.map(id => {
68 return queue.add(async () => {
69 try {
70 console.log(`Surveillance du dinosaure #${id}...`);
71 const result = await fetchDinosaurInfo(id);
72 results.push(result);
73 updateProgress();
74 return result;
75 } catch (error) {
76 errors.push({ id, error: error.message });
77 updateProgress();
78 throw error;
79 }
80 }).catch(error => {
81 // Capturer les erreurs pour ne pas interrompre tout le processus
82 console.error(`Erreur lors de la surveillance du dinosaure #${id} :`, error.message);
83 });
84 });
85
86 // Attendre que toutes les tâches soient terminées
87 await Promise.all(promises);
88
89 return {
90 results,
91 errors,
92 summary: {
93 total: totalTasks,
94 success: results.length,
95 error: errors.length
96 }
97 };
98}Gestion des données en cache (Caching)
Au Parc Jurassique, nous avons des données qui changent rarement (par ex. les espèces de dinosaures) et des données qui changent fréquemment (par ex. la localisation des dinosaures). Implémenter un cache aidera à optimiser la communication :
1class CacheManager {
2 constructor() {
3 this.cache = new Map();
4 this.stats = {
5 hits: 0,
6 misses: 0,
7 invalidations: 0
8 };
9 }
10
11 /**
12 * Récupère les données du cache ou exécute la fonction de récupération si les données ne sont pas en cache
13 * @param {string} key - Identifiant unique des données dans le cache
14 * @param {Function} fetchFn - Fonction pour récupérer les données si absentes du cache
15 * @param {Object} options - Options du cache
16 * @returns {Promise} - Promise avec les données
17 */
18 async get(key, fetchFn, options = {}) {
19 const {
20 ttl = 60000, // TTL par défaut : 1 minute
21 forceRefresh = false, // Forcer le rafraîchissement même si en cache
22 onlyIfCached = false // Retourner uniquement depuis le cache (pas de requête réseau)
23 } = options;
24
25 // Vérifier si les données sont en cache et toujours valides
26 const entry = this.cache.get(key);
27 const now = Date.now();
28
29 if (entry && !forceRefresh && now < entry.expiresAt) {
30 // Données trouvées en cache et toujours valides
31 this.stats.hits++;
32 return entry.data;
33 }
34
35 // Si on ne veut que des données en cache et qu'il n'y en a pas, retourner null
36 if (onlyIfCached) {
37 return null;
38 }
39
40 // Données absentes du cache ou rafraîchissement forcé
41 this.stats.misses++;
42
43 try {
44 // Récupérer les données
45 const data = await fetchFn();
46
47 // Enregistrer en cache
48 this.cache.set(key, {
49 data,
50 expiresAt: now + ttl,
51 addedAt: now
52 });
53
54 return data;
55 } catch (error) {
56 // Si on a des données périmées en cache, on peut les retourner en secours
57 if (entry) {
58 console.warn(`Erreur lors de la récupération des données pour ${key}, utilisation des données périmées du cache :`, error);
59 return entry.data;
60 }
61
62 // Sinon, propager l'erreur
63 throw error;
64 }
65 }
66
67 /**
68 * Invalide les entrées du cache correspondant à un motif
69 * @param {string|RegExp} pattern - Motif pour faire correspondre les clés du cache
70 */
71 invalidate(pattern) {
72 const isString = typeof pattern === 'string';
73 let count = 0;
74
75 for (const key of this.cache.keys()) {
76 if (isString ? key.includes(pattern) : pattern.test(key)) {
77 this.cache.delete(key);
78 count++;
79 }
80 }
81
82 this.stats.invalidations += count;
83 return count;
84 }
85
86 /**
87 * Vide toutes les entrées du cache
88 */
89 clear() {
90 const count = this.cache.size;
91 this.cache.clear();
92 this.stats.invalidations += count;
93 return count;
94 }
95
96 /**
97 * Retourne les statistiques d'utilisation du cache
98 */
99 getStats() {
100 const totalRequests = this.stats.hits + this.stats.misses;
101 const hitRate = totalRequests === 0 ? 0 : this.stats.hits / totalRequests;
102
103 return {
104 ...this.stats,
105 totalRequests,
106 hitRate: hitRate.toFixed(2),
107 activeEntries: this.cache.size
108 };
109 }
110}
111
112// Créer une instance du gestionnaire de cache
113const dinosaurCache = new CacheManager();
114
115// Exemple d'utilisation
116async function fetchDinosaurInfoWithCache(id, options = {}) {
117 return dinosaurCache.get(
118 `dino-${id}`,
119 async () => {
120 const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`);
121 if (!response.ok) {
122 throw new Error(`Erreur HTTP : ${response.status}`);
123 }
124 return response.json();
125 },
126 {
127 ttl: 300000, // 5 minutes pour les données de dinosaures
128 ...options
129 }
130 );
131}
132
133// Fonction pour invalider le cache après une mise à jour
134async function updateDinosaur(id, data) {
135 const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
136 method: 'PUT',
137 headers: { 'Content-Type': 'application/json' },
138 body: JSON.stringify(data)
139 });
140
141 if (!response.ok) {
142 throw new Error(`Erreur de mise à jour : ${response.status}`);
143 }
144
145 // Invalider le cache de ce dinosaure
146 dinosaurCache.invalidate(`dino-${id}`);
147
148 // Invalider aussi les listes de dinosaures qui pourraient contenir ce dinosaure
149 dinosaurCache.invalidate('dino-list');
150
151 return response.json();
152}
153
154// Récupérer la liste des dinosaures avec un cache plus court (les données changent plus souvent)
155async function getDinosaurList(filter = {}) {
156 const queryParams = new URLSearchParams(filter).toString();
157 const cacheKey = `dino-list-${queryParams || 'all'}`;
158
159 return dinosaurCache.get(
160 cacheKey,
161 async () => {
162 const url = `https://api.jurassic-park.com/dinosaurs?${queryParams}`;
163 const response = await fetch(url);
164 if (!response.ok) {
165 throw new Error(`Erreur HTTP : ${response.status}`);
166 }
167 return response.json();
168 },
169 { ttl: 60000 } // 1 minute pour les listes de dinosaures
170 );
171}Gestion des requêtes synchronisées avec le serveur
Au Parc Jurassique, nous devons parfois effectuer des opérations qui nécessitent une synchronisation avec le serveur, comme la mise à jour de l'état de santé des dinosaures. Implémentons un mécanisme qui garantit l'enregistrement des données même en cas de problèmes de connectivité :
1class SyncQueue {
2 constructor(options = {}) {
3 // Clé localStorage pour stocker la file
4 this.key = options.key || 'dinosaurs-sync-queue';
5
6 // Charger la file enregistrée
7 this.queue = this.loadQueue();
8
9 // Indicateur précisant si la synchronisation est active
10 this.isSyncing = false;
11
12 // Démarrage automatique de la synchronisation
13 if (options.autoSync !== false) {
14 this.startSync();
15 }
16
17 // Écouter les changements d'état online/offline
18 window.addEventListener('online', () => {
19 console.log('Connexion rétablie. Démarrage de la synchronisation...');
20 this.startSync();
21 });
22
23 window.addEventListener('offline', () => {
24 console.log('Connexion perdue. Synchronisation en pause.');
25 });
26 }
27
28 /**
29 * Charge la file depuis localStorage
30 */
31 loadQueue() {
32 try {
33 const savedQueue = localStorage.getItem(this.key);
34 return savedQueue ? JSON.parse(savedQueue) : [];
35 } catch (error) {
36 console.error('Erreur lors du chargement de la file de synchronisation :', error);
37 return [];
38 }
39 }
40
41 /**
42 * Enregistre la file dans localStorage
43 */
44 saveQueue() {
45 try {
46 localStorage.setItem(this.key, JSON.stringify(this.queue));
47 } catch (error) {
48 console.error('Erreur lors de l\'enregistrement de la file de synchronisation :', error);
49 }
50 }
51
52 /**
53 * Ajoute une opération à la file de synchronisation
54 * @param {string} url - URL de la requête
55 * @param {Object} options - Options de Fetch
56 * @param {string} type - Type d'opération (par ex. 'mise-a-jour-sante')
57 * @returns {string} - ID de l'opération dans la file
58 */
59 addToQueue(url, options = {}, type = 'inconnu') {
60 const id = `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
61
62 this.queue.push({
63 id,
64 url,
65 options,
66 type,
67 addedAt: new Date().toISOString(),
68 attempts: 0
69 });
70
71 this.saveQueue();
72 this.startSync();
73
74 return id;
75 }
76
77 /**
78 * Démarre le processus de synchronisation
79 */
80 async startSync() {
81 // Si la synchronisation est déjà en cours ou pas de connexion, ne rien faire
82 if (this.isSyncing || !navigator.onLine || this.queue.length === 0) {
83 return;
84 }
85
86 this.isSyncing = true;
87
88 try {
89 let index = 0;
90 while (index < this.queue.length) {
91 const operation = this.queue[index];
92
93 try {
94 console.log(`Synchronisation de l'opération : ${operation.type} (${operation.id})`);
95
96 // Incrémenter le nombre de tentatives
97 operation.attempts++;
98 this.saveQueue();
99
100 // Exécuter la requête
101 const response = await fetch(operation.url, operation.options);
102
103 if (!response.ok) {
104 throw new Error(`Erreur HTTP : ${response.status}`);
105 }
106
107 // Opération réussie, retirer de la file
108 this.queue.splice(index, 1);
109 this.saveQueue();
110
111 console.log(`Opération ${operation.id} synchronisée avec succès`);
112
113 // Ne pas incrémenter index car on a retiré l'élément
114 } catch (error) {
115 console.error(`Erreur lors de la synchronisation de l'opération ${operation.id} :`, error);
116
117 // Si on a atteint le nombre max de tentatives, marquer comme échouée
118 if (operation.attempts >= 5) {
119 console.warn(`L'opération ${operation.id} a dépassé la limite de tentatives. Marquée comme échouée.`);
120 operation.status = 'échouée';
121 operation.errorMessage = error.message;
122 this.saveQueue();
123 }
124
125 // Passer à l'opération suivante
126 index++;
127 }
128 }
129 } finally {
130 this.isSyncing = false;
131
132 // S'il reste des éléments dans la file, réessayer dans 1 minute
133 if (this.queue.some(op => !op.status || op.status !== 'échouée')) {
134 setTimeout(() => this.startSync(), 60000);
135 }
136 }
137 }
138
139 /**
140 * Retire les opérations échouées de la file
141 */
142 clearFailedOperations() {
143 const previousCount = this.queue.length;
144 this.queue = this.queue.filter(op => !op.status || op.status !== 'échouée');
145 this.saveQueue();
146 return previousCount - this.queue.length;
147 }
148
149 /**
150 * Retourne l'état actuel de la file
151 */
152 getStatus() {
153 const total = this.queue.length;
154 const failed = this.queue.filter(op => op.status === 'échouée').length;
155 const pending = total - failed;
156
157 return {
158 total,
159 pending,
160 failed,
161 isSyncing: this.isSyncing
162 };
163 }
164}
165
166// Créer une instance de la file de synchronisation
167const dinosaurSyncQueue = new SyncQueue({
168 key: 'jurassic-park-dino-sync'
169});
170
171// Exemple d'utilisation : mise à jour de la santé d'un dinosaure qui sera synchronisée
172function updateDinosaurHealth(id, healthParams) {
173 // Préparer les données à envoyer
174 const data = {
175 id,
176 health: healthParams.health,
177 lastCheckTime: new Date().toISOString(),
178 symptoms: healthParams.symptoms || [],
179 updatedBy: 'dr-wu'
180 };
181
182 // Si on est en ligne, essayer immédiatement
183 if (navigator.onLine) {
184 return fetch(`https://api.jurassic-park.com/dinosaurs/${id}/health`, {
185 method: 'PUT',
186 headers: { 'Content-Type': 'application/json' },
187 body: JSON.stringify(data)
188 })
189 .then(response => {
190 if (!response.ok) {
191 throw new Error(`Erreur HTTP : ${response.status}`);
192 }
193 return response.json();
194 })
195 .catch(error => {
196 console.warn('Échec de la mise à jour de la santé en ligne, ajout à la file de synchronisation');
197
198 // Ajouter à la file de synchronisation
199 dinosaurSyncQueue.addToQueue(
200 `https://api.jurassic-park.com/dinosaurs/${id}/health`,
201 {
202 method: 'PUT',
203 headers: { 'Content-Type': 'application/json' },
204 body: JSON.stringify(data)
205 },
206 `mise-a-jour-sante-${id}`
207 );
208
209 // Retourner les données comme si l'opération avait réussi, bien qu'elle soit synchronisée plus tard
210 return {
211 ...data,
212 pendingSync: true
213 };
214 });
215 } else {
216 // Si on est hors ligne, ajouter immédiatement à la file
217 dinosaurSyncQueue.addToQueue(
218 `https://api.jurassic-park.com/dinosaurs/${id}/health`,
219 {
220 method: 'PUT',
221 headers: { 'Content-Type': 'application/json' },
222 body: JSON.stringify(data)
223 },
224 `mise-a-jour-sante-${id}`
225 );
226
227 return Promise.resolve({
228 ...data,
229 pendingSync: true
230 });
231 }
232}Authentification et rafraîchissement du token
Dans le système du Parc Jurassique, nous devons sécuriser l'accès à l'API à l'aide de tokens d'authentification. Implémentons un mécanisme qui rafraîchit automatiquement le token lorsqu'il expire :
1class AuthManager {
2 constructor() {
3 this.tokenAccessKey = 'jp-access-token';
4 this.tokenRefreshKey = 'jp-refresh-token';
5 this.expiryKey = 'jp-token-expiry';
6 this.refreshInProgress = false;
7 this.refreshListeners = [];
8 }
9
10 /**
11 * Récupère le token d'accès actuel
12 * @param {boolean} autoRefresh - Rafraîchir automatiquement le token s'il a expiré
13 * @returns {Promise<string>} - Promise avec le token d'accès
14 */
15 async getAccessToken(autoRefresh = true) {
16 const token = localStorage.getItem(this.tokenAccessKey);
17 const expiry = localStorage.getItem(this.expiryKey);
18
19 // Vérifier si le token existe et n'a pas expiré
20 if (token && expiry) {
21 const expiryDate = new Date(expiry);
22 const now = new Date();
23
24 // Ajouter une marge de 1 minute pour éviter d'utiliser un token sur le point d'expirer
25 const bufferTime = 60 * 1000; // 1 minute en millisecondes
26
27 if (expiryDate.getTime() - now.getTime() > bufferTime) {
28 return token;
29 }
30
31 // Le token a expiré ou va bientôt expirer
32 if (autoRefresh) {
33 return this.refreshToken();
34 }
35 }
36
37 throw new Error('Aucun token d\'accès valide');
38 }
39
40 /**
41 * Rafraîchit le token d'accès à l'aide du refresh token
42 * @returns {Promise<string>} - Promise avec le nouveau token d'accès
43 */
44 async refreshToken() {
45 // Si un rafraîchissement est déjà en cours, attendre son résultat
46 if (this.refreshInProgress) {
47 return new Promise((resolve, reject) => {
48 this.refreshListeners.push({ resolve, reject });
49 });
50 }
51
52 this.refreshInProgress = true;
53
54 try {
55 const refreshToken = localStorage.getItem(this.tokenRefreshKey);
56
57 if (!refreshToken) {
58 throw new Error('Aucun refresh token');
59 }
60
61 const response = await fetch('https://api.jurassic-park.com/auth/refresh', {
62 method: 'POST',
63 headers: { 'Content-Type': 'application/json' },
64 body: JSON.stringify({ refresh_token: refreshToken })
65 });
66
67 if (!response.ok) {
68 if (response.status === 401) {
69 // Le refresh token est invalide, déconnecter l'utilisateur
70 this.logout();
71 throw new Error('Session expirée. Merci de te reconnecter.');
72 }
73 throw new Error(`Erreur de rafraîchissement du token : ${response.status}`);
74 }
75
76 const data = await response.json();
77
78 // Enregistrer les nouveaux tokens
79 localStorage.setItem(this.tokenAccessKey, data.access_token);
80 localStorage.setItem(this.tokenRefreshKey, data.refresh_token);
81
82 // Calculer le temps d'expiration
83 const expiry = new Date();
84 expiry.setSeconds(expiry.getSeconds() + data.expires_in);
85 localStorage.setItem(this.expiryKey, expiry.toISOString());
86
87 // Notifier tous les écouteurs en attente
88 this.refreshListeners.forEach(listener => {
89 listener.resolve(data.access_token);
90 });
91
92 this.refreshListeners = [];
93 return data.access_token;
94 } catch (error) {
95 // En cas d'erreur, notifier tous les écouteurs en attente
96 this.refreshListeners.forEach(listener => {
97 listener.reject(error);
98 });
99
100 this.refreshListeners = [];
101 throw error;
102 } finally {
103 this.refreshInProgress = false;
104 }
105 }
106
107 /**
108 * Enregistre les tokens après connexion
109 * @param {Object} authData - Données d'authentification
110 */
111 saveTokens(authData) {
112 localStorage.setItem(this.tokenAccessKey, authData.access_token);
113 localStorage.setItem(this.tokenRefreshKey, authData.refresh_token);
114
115 // Calculer le temps d'expiration
116 const expiry = new Date();
117 expiry.setSeconds(expiry.getSeconds() + authData.expires_in);
118 localStorage.setItem(this.expiryKey, expiry.toISOString());
119 }
120
121 /**
122 * Déconnecte l'utilisateur
123 */
124 logout() {
125 localStorage.removeItem(this.tokenAccessKey);
126 localStorage.removeItem(this.tokenRefreshKey);
127 localStorage.removeItem(this.expiryKey);
128 }
129
130 /**
131 * Vérifie si l'utilisateur est connecté
132 * @returns {boolean}
133 */
134 isLoggedIn() {
135 return !!localStorage.getItem(this.tokenAccessKey);
136 }
137}
138
139// Créer une instance du gestionnaire d'authentification
140const auth = new AuthManager();
141
142// Exemple d'utilisation - fonction pour effectuer des requêtes authentifiées
143async function authenticatedRequest(url, options = {}) {
144 try {
145 // Récupérer le token d'accès (avec rafraîchissement automatique)
146 const token = await auth.getAccessToken();
147
148 // Ajouter le token aux en-têtes
149 const authOptions = {
150 ...options,
151 headers: {
152 ...options.headers,
153 'Authorization': `Bearer ${token}`
154 }
155 };
156
157 // Exécuter la requête
158 const response = await fetch(url, authOptions);
159
160 // Gérer les erreurs d'autorisation
161 if (response.status === 401) {
162 // Essayer de rafraîchir le token et de réessayer
163 try {
164 const newToken = await auth.refreshToken();
165
166 // Réessayer la requête avec le nouveau token
167 const refreshedOptions = {
168 ...options,
169 headers: {
170 ...options.headers,
171 'Authorization': `Bearer ${newToken}`
172 }
173 };
174
175 return fetch(url, refreshedOptions);
176 } catch (refreshError) {
177 // Le rafraîchissement du token a échoué, déconnecter l'utilisateur
178 auth.logout();
179 window.location.href = '/login'; // Rediriger vers la connexion
180 throw new Error('Session expirée. Merci de te reconnecter.');
181 }
182 }
183
184 return response;
185 } catch (error) {
186 console.error('Erreur de requête authentifiée :', error);
187 throw error;
188 }
189}
190
191// Exemple d'utilisation
192async function fetchEnclosureData(enclosureId) {
193 const response = await authenticatedRequest(
194 `https://api.jurassic-park.com/paddocks/${enclosureId}`
195 );
196
197 if (!response.ok) {
198 throw new Error(`Erreur lors de la récupération des données de l'enclos : ${response.status}`);
199 }
200
201 return response.json();
202}
203
204// Fonction de connexion
205async function login(email, password) {
206 try {
207 const response = await fetch('https://api.jurassic-park.com/auth/login', {
208 method: 'POST',
209 headers: { 'Content-Type': 'application/json' },
210 body: JSON.stringify({ email, password: password })
211 });
212
213 if (!response.ok) {
214 throw new Error(`Erreur de connexion : ${response.status}`);
215 }
216
217 const authData = await response.json();
218
219 // Enregistrer les tokens
220 auth.saveTokens(authData);
221
222 return {
223 success: true,
224 user: authData.user
225 };
226 } catch (error) {
227 console.error('Erreur de connexion :', error);
228 return {
229 success: false,
230 error: error.message
231 };
232 }
233}Téléversement de fichiers avec progression
Au Parc Jurassique, nous devons souvent téléverser de gros fichiers, comme des images ou des vidéos des caméras de surveillance des dinosaures. Implémentons une fonction qui gère le téléversement de fichiers avec rapport de progression :
1/**
2 * Envoie un fichier au serveur avec suivi de progression
3 * @param {File} file - Fichier à envoyer
4 * @param {string} url - URL cible
5 * @param {Object} options - Options supplémentaires
6 * @returns {Promise} - Promise avec le résultat de l'opération
7 */
8async function sendFile(file, url, options = {}) {
9 return new Promise((resolve, reject) => {
10 // Options par défaut
11 const {
12 method = 'POST',
13 fieldName = 'file',
14 additionalData = {},
15 onProgress = null,
16 onError = null,
17 maxRetries = 3
18 } = options;
19
20 // Compteur de tentatives
21 let attempts = 0;
22
23 // Fonction pour exécuter le téléversement
24 function performUpload() {
25 // Créer un objet XHR
26 const xhr = new XMLHttpRequest();
27
28 // Configurer les gestionnaires d'événements
29 xhr.upload.addEventListener('progress', (event) => {
30 if (event.lengthComputable && typeof onProgress === 'function') {
31 const percent = Math.round((event.loaded / event.total) * 100);
32 onProgress({
33 loaded: event.loaded,
34 total: event.total,
35 percent
36 });
37 }
38 });
39
40 xhr.addEventListener('load', () => {
41 if (xhr.status >= 200 && xhr.status < 300) {
42 try {
43 // Analyser la réponse JSON
44 const responseData = JSON.parse(xhr.responseText);
45 resolve(responseData);
46 } catch (e) {
47 resolve({
48 success: true,
49 status: xhr.status,
50 message: xhr.statusText
51 });
52 }
53 } else {
54 const error = new Error(`Erreur HTTP : ${xhr.status}`);
55 error.status = xhr.status;
56 error.statusText = xhr.statusText;
57
58 if (xhr.status >= 500 && attempts < maxRetries) {
59 // Pour les erreurs serveur, réessayer
60 attempts++;
61 console.warn(`Erreur de téléversement (${xhr.status}). Nouvelle tentative ${attempts}/${maxRetries}...`);
62 setTimeout(performUpload, 2000 * attempts); // Délai croissant entre les tentatives
63 } else {
64 if (typeof onError === 'function') {
65 onError(error);
66 }
67 reject(error);
68 }
69 }
70 });
71
72 xhr.addEventListener('error', () => {
73 const error = new Error('Erreur réseau pendant le téléversement du fichier');
74 if (typeof onError === 'function') {
75 onError(error);
76 }
77
78 if (attempts < maxRetries) {
79 attempts++;
80 console.warn(`Erreur réseau pendant le téléversement. Nouvelle tentative ${attempts}/${maxRetries}...`);
81 setTimeout(performUpload, 2000 * attempts);
82 } else {
83 reject(error);
84 }
85 });
86
87 xhr.addEventListener('abort', () => {
88 const error = new Error('Le téléversement du fichier a été annulé');
89 if (typeof onError === 'function') {
90 onError(error);
91 }
92 reject(error);
93 });
94
95 // Ouvrir la connexion
96 xhr.open(method, url, true);
97
98 // Préparer FormData
99 const formData = new FormData();
100 formData.append(fieldName, file);
101
102 // Ajouter les données supplémentaires
103 Object.entries(additionalData).forEach(([key, value]) => {
104 formData.append(key, value);
105 });
106
107 // Envoyer la requête
108 xhr.send(formData);
109
110 // Retourner un objet avec une méthode d'annulation
111 return {
112 abort: () => xhr.abort()
113 };
114 }
115
116 // Démarrer le téléversement
117 performUpload();
118 });
119}
120
121// Exemple d'utilisation
122async function sendCameraFootage(file, location) {
123 const controller = document.getElementById('upload-controller');
124 const progressBar = document.getElementById('upload-progress');
125 const statusText = document.getElementById('upload-status');
126
127 controller.classList.remove('hidden');
128 statusText.textContent = 'Préparation...';
129
130 try {
131 const result = await sendFile(
132 file,
133 'https://api.jurassic-park.com/security/camera-footage',
134 {
135 additionalData: {
136 location: location,
137 timestamp: new Date().toISOString(),
138 operator: 'dr-wu'
139 },
140 onProgress: (info) => {
141 progressBar.value = info.percent;
142 statusText.textContent = `Téléversement : ${info.percent}%`;
143 },
144 onError: (error) => {
145 statusText.textContent = `Erreur : ${error.message}`;
146 progressBar.classList.add('error');
147 }
148 }
149 );
150
151 statusText.textContent = 'Téléversement terminé !';
152 progressBar.value = 100;
153
154 setTimeout(() => {
155 controller.classList.add('hidden');
156 }, 3000);
157
158 return result;
159 } catch (error) {
160 statusText.textContent = `Erreur : ${error.message}`;
161 progressBar.classList.add('error');
162 throw error;
163 }
164}Surveillance de l'état du réseau
Au Parc Jurassique, nous devons être préparés aux problèmes de connectivité, surtout dans les zones reculées de l'île. Implémentons un système de surveillance de l'état du réseau :
1class NetworkMonitor {
2 constructor(options = {}) {
3 this.listeners = {
4 change: [],
5 online: [],
6 offline: []
7 };
8
9 this.lastPing = null;
10 this.delayTime = null;
11 this.status = navigator.onLine ? 'online' : 'offline';
12 this.endpointUrl = options.endpointPing || 'https://api.jurassic-park.com/ping';
13 this.pingIntervalMs = options.pingIntervalMs || 30000; // 30 secondes
14 this.pingTimeout = options.pingTimeout || 5000; // 5 secondes
15
16 // Ajouter les écouteurs pour les événements online/offline
17 window.addEventListener('online', this.handleOnline.bind(this));
18 window.addEventListener('offline', this.handleOffline.bind(this));
19
20 // Démarrer les pings réguliers
21 if (options.autostartPing !== false) {
22 this.startPinging();
23 }
24 }
25
26 /**
27 * Gère la transition vers le mode en ligne
28 */
29 handleOnline() {
30 const previousStatus = this.status;
31 this.status = 'online';
32
33 // Notifier les écouteurs
34 this.notifyListeners('online');
35
36 if (previousStatus !== this.status) {
37 this.notifyListeners('change', { status: this.status, previous: previousStatus });
38 }
39
40 // Effectuer un ping immédiat pour vérifier la connectivité réelle
41 this.ping();
42 }
43
44 /**
45 * Gère la transition vers le mode hors ligne
46 */
47 handleOffline() {
48 const previousStatus = this.status;
49 this.status = 'offline';
50 this.delayTime = null;
51
52 // Notifier les écouteurs
53 this.notifyListeners('offline');
54
55 if (previousStatus !== this.status) {
56 this.notifyListeners('change', { status: this.status, previous: previousStatus });
57 }
58 }
59
60 /**
61 * Ping le serveur pour vérifier la connectivité réelle
62 * @returns {Promise<number>} - Promise avec la latence en ms
63 */
64 async ping() {
65 if (!navigator.onLine) {
66 this.delayTime = null;
67 return Promise.reject(new Error('Le navigateur est en mode hors ligne'));
68 }
69
70 try {
71 const startTime = Date.now();
72
73 // Définir un timeout pour fetch
74 const controller = new AbortController();
75 const timeoutId = setTimeout(() => controller.abort(), this.pingTimeout);
76
77 const response = await fetch(`${this.endpointUrl}?t=${startTime}`, {
78 method: 'GET',
79 cache: 'no-store',
80 signal: controller.signal
81 });
82
83 clearTimeout(timeoutId);
84
85 if (!response.ok) {
86 throw new Error(`Erreur HTTP : ${response.status}`);
87 }
88
89 // Calculer la latence
90 const endTime = Date.now();
91 this.delayTime = endTime - startTime;
92 this.lastPing = new Date();
93
94 // Mettre à jour le statut
95 const previousStatus = this.status;
96 this.status = 'online';
97
98 if (previousStatus !== this.status) {
99 this.notifyListeners('change', { status: this.status, previous: previousStatus });
100 this.notifyListeners('online');
101 }
102
103 return this.delayTime;
104 } catch (error) {
105 // Si timeout ou erreur réseau, marquer comme hors ligne
106 if (error.name === 'AbortError' || error instanceof TypeError) {
107 const previousStatus = this.status;
108 this.status = 'offline';
109 this.delayTime = null;
110
111 if (previousStatus !== this.status) {
112 this.notifyListeners('change', { status: this.status, previous: previousStatus });
113 this.notifyListeners('offline');
114 }
115 }
116
117 throw error;
118 }
119 }
120
121 /**
122 * Démarre les pings réguliers du serveur
123 */
124 startPinging() {
125 // Arrêter l'intervalle existant, le cas échéant
126 this.stopPinging();
127
128 // Effectuer un premier ping immédiatement
129 this.ping().catch(e => console.warn('Erreur du ping initial :', e));
130
131 // Définir l'intervalle de ping
132 this.pingInterval = setInterval(() => {
133 this.ping().catch(e => console.warn('Erreur de ping :', e));
134 }, this.pingIntervalMs);
135 }
136
137 /**
138 * Arrête les pings réguliers du serveur
139 */
140 stopPinging() {
141 if (this.pingInterval) {
142 clearInterval(this.pingInterval);
143 this.pingInterval = null;
144 }
145 }
146
147 /**
148 * Ajoute un écouteur d'événement
149 * @param {string} eventType - Type d'événement ('change', 'online', 'offline')
150 * @param {Function} callback - Fonction callback
151 */
152 on(eventType, callback) {
153 if (this.listeners[eventType]) {
154 this.listeners[eventType].push(callback);
155 }
156 return this; // Pour une API en chaîne
157 }
158
159 /**
160 * Retire un écouteur
161 * @param {string} eventType - Type d'événement
162 * @param {Function} callback - Fonction callback à retirer
163 */
164 off(eventType, callback) {
165 if (this.listeners[eventType]) {
166 this.listeners[eventType] = this.listeners[eventType].filter(cb => cb !== callback);
167 }
168 return this;
169 }
170
171 /**
172 * Notifie les écouteurs d'un événement
173 * @param {string} eventType - Type d'événement
174 * @param {Object} data - Données de l'événement
175 */
176 notifyListeners(eventType, data = {}) {
177 if (this.listeners[eventType]) {
178 this.listeners[eventType].forEach(callback => {
179 try {
180 callback({
181 type: eventType,
182 timestamp: new Date(),
183 status: this.status,
184 delayTime: this.delayTime,
185 ...data
186 });
187 } catch (e) {
188 console.error(`Erreur dans l'écouteur ${eventType} :`, e);
189 }
190 });
191 }
192 }
193
194 /**
195 * Retourne l'état actuel du réseau
196 */
197 getState() {
198 return {
199 status: this.status,
200 delayTime: this.delayTime,
201 lastPing: this.lastPing,
202 browserOnline: navigator.onLine
203 };
204 }
205}
206
207// Créer une instance du moniteur réseau
208const networkMonitor = new NetworkMonitor();
209
210// Exemple d'utilisation
211networkMonitor.on('change', (info) => {
212 console.log(`Changement d'état du réseau : ${info.previous} -> ${info.status}`);
213
214 // Mettre à jour l'interface
215 const statusIndicator = document.getElementById('network-status');
216 if (statusIndicator) {
217 statusIndicator.className = `status-${info.status}`;
218 statusIndicator.textContent = info.status === 'online'
219 ? `En ligne (ping : ${info.delayTime}ms)`
220 : 'Hors ligne';
221 }
222});
223
224networkMonitor.on('offline', () => {
225 // Afficher une notification à l'utilisateur
226 showNotification('Connexion réseau perdue', 'error');
227
228 // Activer le mode hors ligne dans l'application
229 app.enableOfflineMode();
230});
231
232networkMonitor.on('online', (info) => {
233 showNotification(`Connexion rétablie (ping : ${info.delayTime}ms)`, 'success');
234
235 // Synchroniser les données avec le serveur
236 dinosaurSyncQueue.startSync();
237
238 // Désactiver le mode hors ligne
239 app.disableOfflineMode();
240});
241
242function showNotification(text, type) {
243 const notification = document.createElement('div');
244 notification.className = `notification ${type}`;
245 notification.textContent = text;
246
247 document.body.appendChild(notification);
248
249 // Retirer la notification après 5 secondes
250 setTimeout(() => {
251 notification.classList.add('fade-out');
252 setTimeout(() => {
253 document.body.removeChild(notification);
254 }, 500);
255 }, 5000);
256}Résumé - Construire un client API complet
En combinant toutes les techniques abordées, nous pouvons créer un système de communication API très performant, résistant aux erreurs et convivial. Voici une solution complète :
1/**
2 * Client API complet pour le système du Parc Jurassique
3 */
4class JurassicAPIClient {
5 constructor(options = {}) {
6 // Configuration de l'URL de base
7 this.baseUrl = options.baseUrl || 'https://api.jurassic-park.com';
8
9 // Initialiser le gestionnaire d'authentification
10 this.auth = new AuthManager();
11
12 // Initialiser le cache
13 this.cache = new CacheManager();
14
15 // Initialiser la file de synchronisation
16 this.syncQueue = new SyncQueue({
17 key: 'jp-sync-queue',
18 autoSync: true
19 });
20
21 // Initialiser le moniteur réseau
22 this.networkMonitor = new NetworkMonitor({
23 endpointPing: `${this.baseUrl}/ping`,
24 pingIntervalMs: 30000
25 });
26
27 // Surveiller l'état du réseau
28 this.networkMonitor.on('online', () => {
29 this.syncQueue.startSync();
30 });
31
32 // Options de requête par défaut
33 this.defaultOptions = {
34 timeout: 30000,
35 retries: 3,
36 cacheTime: 300000, // 5 minutes
37 useCache: true
38 };
39 }
40
41 /**
42 * Exécute une requête HTTP
43 * @param {string} endpoint - Endpoint de l'API
44 * @param {Object} options - Options de la requête
45 * @returns {Promise} - Promise avec le résultat de la requête
46 */
47 async request(endpoint, options = {}) {
48 // Fusionner les options par défaut avec celles fournies
49 const {
50 method = 'GET',
51 body,
52 headers = {},
53 useAuth = true,
54 useCache = this.defaultOptions.useCache,
55 cacheTime = this.defaultOptions.cacheTime,
56 forceRefresh = false,
57 timeout = this.defaultOptions.timeout,
58 retries = this.defaultOptions.retries,
59 offlineFallback = true
60 } = options;
61
62 // Préparer la clé de cache
63 const cacheKey = `${method}:${endpoint}:${JSON.stringify(body || {})}`;
64
65 // Vérifier si la requête est en cache (uniquement pour GET)
66 if (method === 'GET' && useCache && !forceRefresh) {
67 try {
68 const cachedData = await this.cache.get(
69 cacheKey,
70 () => {}, // Fonction vide - on ne veut pas encore exécuter la requête
71 { ttl: cacheTime, onlyIfCached: true }
72 );
73
74 if (cachedData) {
75 return cachedData;
76 }
77 } catch (error) {
78 // Ignorer les erreurs de cache
79 }
80 }
81
82 // Vérifier l'état du réseau
83 if (!this.networkMonitor.getState().status === 'online') {
84 // On est hors ligne, vérifier si on a des données en cache
85 if (method === 'GET' && useCache && offlineFallback) {
86 try {
87 const cachedData = await this.cache.get(
88 cacheKey,
89 () => {}, // Fonction vide - on ne veut pas encore exécuter la requête
90 { ttl: Infinity, onlyIfCached: true, ignoreExpiry: true }
91 );
92
93 if (cachedData) {
94 // Information sur le mode hors ligne
95 return {
96 ...cachedData,
97 _meta: {
98 fromCache: true,
99 offlineMode: true,
100 timestamp: new Date()
101 }
102 };
103 }
104 } catch (error) {
105 // Ignorer les erreurs de cache
106 }
107 }
108
109 // S'il s'agit d'une requête modifiant les données, l'ajouter à la file de synchronisation
110 if (method !== 'GET' && offlineFallback) {
111 return this.addToSyncQueue(endpoint, { method, body, headers, useAuth });
112 }
113
114 throw new Error('Pas de connexion réseau. Impossible d\'exécuter la requête.');
115 }
116
117 // Fonction qui exécute la requête réelle
118 const executeRequest = async (signal) => {
119 // Préparer l'URL
120 const url = `${this.baseUrl}${endpoint}`;
121
122 // Préparer les options de la requête
123 const requestOptions = {
124 method,
125 headers: {
126 'Content-Type': 'application/json',
127 ...headers
128 },
129 signal
130 };
131
132 // Ajouter le corps de la requête s'il existe
133 if (body) {
134 requestOptions.body = JSON.stringify(body);
135 }
136
137 // Ajouter le token d'authentification si requis
138 if (useAuth) {
139 try {
140 const token = await this.auth.getAccessToken();
141 requestOptions.headers['Authorization'] = `Bearer ${token}`;
142 } catch (error) {
143 // Si impossible d'obtenir le token et que la requête nécessite l'auth
144 throw new Error('Non authentifié. Merci de te reconnecter.');
145 }
146 }
147
148 // Exécuter la requête avec timeout
149 const controller = new AbortController();
150 const { signal: abortSignal } = controller;
151 if (signal) {
152 // Si un signal externe a été passé, le combiner avec le nôtre
153 signal.addEventListener('abort', () => controller.abort());
154 }
155
156 const timeoutId = setTimeout(() => controller.abort(), timeout);
157 requestOptions.signal = abortSignal;
158
159 try {
160 const response = await fetch(url, requestOptions);
161 clearTimeout(timeoutId);
162
163 // Gérer les erreurs HTTP
164 if (response.status === 401 && useAuth) {
165 // Essayer de rafraîchir le token
166 try {
167 await this.auth.refreshToken();
168 // Réessayer avec le nouveau token
169 return this.request(endpoint, options);
170 } catch (refreshError) {
171 throw new Error('Session expirée. Merci de te reconnecter.');
172 }
173 }
174
175 if (!response.ok) {
176 const error = new Error(`Erreur HTTP : ${response.status}`);
177 error.status = response.status;
178 error.statusText = response.statusText;
179
180 // Essayer de récupérer les détails de l'erreur depuis la réponse
181 try {
182 const errorData = await response.json();
183 error.data = errorData;
184 error.message = errorData.message || error.message;
185 } catch (e) {
186 // Ignorer les erreurs d'analyse
187 }
188
189 throw error;
190 }
191
192 // Analyser la réponse selon son type
193 const contentType = response.headers.get('content-type');
194 let data;
195
196 if (contentType && contentType.includes('application/json')) {
197 data = await response.json();
198 } else if (contentType && contentType.includes('text/')) {
199 data = await response.text();
200 } else {
201 data = await response.blob();
202 }
203
204 // Enregistrer la réponse en cache pour les requêtes GET
205 if (method === 'GET' && useCache) {
206 this.cache.get(cacheKey, () => data, { ttl: cacheTime, forceRefresh: true });
207 }
208
209 return data;
210 } catch (error) {
211 clearTimeout(timeoutId);
212
213 if (error.name === 'AbortError') {
214 throw new Error(`Requête annulée après expiration du délai (${timeout}ms)`);
215 }
216
217 throw error;
218 }
219 };
220
221 // Exécuter la requête avec des nouvelles tentatives
222 return withRetry(
223 (signal) => executeRequest(signal),
224 {
225 maxAttempts: retries,
226 initialDelay: 1000,
227 delayFactor: 2,
228 timeoutMs: timeout
229 }
230 );
231 }
232
233 /**
234 * Ajoute une requête à la file de synchronisation (mode hors ligne)
235 * @param {string} endpoint - Endpoint de l'API
236 * @param {Object} options - Options de la requête
237 * @returns {Promise} - Promise avec un résultat temporaire
238 */
239 addToSyncQueue(endpoint, options) {
240 const { method, body } = options;
241
242 // Générer un ID d'opération unique
243 const operationId = `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
244
245 // Ajouter à la file de synchronisation
246 this.syncQueue.addToQueue(
247 `${this.baseUrl}${endpoint}`,
248 {
249 method,
250 headers: {
251 'Content-Type': 'application/json',
252 // Le token sera ajouté lors de la synchronisation réelle
253 },
254 body: body ? JSON.stringify(body) : undefined
255 },
256 `${method}-${endpoint}`
257 );
258
259 // Retourner une réponse temporaire
260 return Promise.resolve({
261 _meta: {
262 pendingSync: true,
263 operationId,
264 timestamp: new Date(),
265 offlineMode: true
266 },
267 ...body // Retourner les données passées comme résultat temporaire
268 });
269 }
270
271 // Méthodes utilitaires pour différents types de requêtes
272
273 /**
274 * Exécute une requête GET
275 */
276 async get(endpoint, options = {}) {
277 return this.request(endpoint, { ...options, method: 'GET' });
278 }
279
280 /**
281 * Exécute une requête POST
282 */
283 async post(endpoint, body, options = {}) {
284 return this.request(endpoint, { ...options, method: 'POST', body });
285 }
286
287 /**
288 * Exécute une requête PUT
289 */
290 async put(endpoint, body, options = {}) {
291 return this.request(endpoint, { ...options, method: 'PUT', body });
292 }
293
294 /**
295 * Exécute une requête DELETE
296 */
297 async delete(endpoint, options = {}) {
298 return this.request(endpoint, { ...options, method: 'DELETE' });
299 }
300
301 /**
302 * Téléverse un fichier sur le serveur
303 */
304 async uploadFile(endpoint, file, options = {}) {
305 const {
306 fieldName = 'file',
307 additionalData = {},
308 onProgress = null,
309 useAuth = true
310 } = options;
311
312 // Si hors ligne, impossible de téléverser des fichiers
313 if (!this.networkMonitor.getState().status === 'online') {
314 throw new Error('Impossible de téléverser un fichier en mode hors ligne.');
315 }
316
317 // Préparer FormData
318 const formData = new FormData();
319 formData.append(fieldName, file);
320
321 // Ajouter les données supplémentaires
322 Object.entries(additionalData).forEach(([key, value]) => {
323 formData.append(key, JSON.stringify(value));
324 });
325
326 // Options de la requête
327 const requestOptions = {
328 method: 'POST',
329 body: formData
330 };
331
332 // Ajouter le token d'authentification si requis
333 if (useAuth) {
334 try {
335 const token = await this.auth.getAccessToken();
336 requestOptions.headers = {
337 'Authorization': `Bearer ${token}`
338 };
339 } catch (error) {
340 throw new Error('Non authentifié. Merci de te reconnecter.');
341 }
342 }
343
344 return new Promise((resolve, reject) => {
345 const xhr = new XMLHttpRequest();
346
347 xhr.upload.addEventListener('progress', (event) => {
348 if (event.lengthComputable && typeof onProgress === 'function') {
349 const percent = Math.round((event.loaded / event.total) * 100);
350 onProgress({
351 loaded: event.loaded,
352 total: event.total,
353 percent
354 });
355 }
356 });
357
358 xhr.addEventListener('load', () => {
359 if (xhr.status >= 200 && xhr.status < 300) {
360 try {
361 const responseData = JSON.parse(xhr.responseText);
362 resolve(responseData);
363 } catch (e) {
364 resolve({
365 success: true,
366 status: xhr.status,
367 message: xhr.statusText
368 });
369 }
370 } else {
371 const error = new Error(`Erreur HTTP : ${xhr.status}`);
372 error.status = xhr.status;
373 error.statusText = xhr.statusText;
374 reject(error);
375 }
376 });
377
378 xhr.addEventListener('error', () => {
379 reject(new Error('Erreur réseau pendant le téléversement du fichier'));
380 });
381
382 xhr.addEventListener('abort', () => {
383 reject(new Error('Le téléversement du fichier a été annulé'));
384 });
385
386 xhr.open('POST', `${this.baseUrl}${endpoint}`, true);
387
388 if (requestOptions.headers) {
389 Object.entries(requestOptions.headers).forEach(([key, value]) => {
390 xhr.setRequestHeader(key, value);
391 });
392 }
393
394 xhr.send(formData);
395 });
396 }
397
398 /**
399 * Authentifie l'utilisateur
400 */
401 async login(email, password) {
402 try {
403 const response = await this.request('/auth/login', {
404 method: 'POST',
405 body: { email, password: password },
406 useAuth: false
407 });
408
409 // Enregistrer les tokens
410 this.auth.saveTokens(response);
411
412 return {
413 success: true,
414 user: response.user
415 };
416 } catch (error) {
417 return {
418 success: false,
419 error: error.message
420 };
421 }
422 }
423
424 /**
425 * Déconnecte l'utilisateur
426 */
427 logout() {
428 this.auth.logout();
429 // Optionnellement, vider le cache à la déconnexion
430 this.cache.clear();
431 }
432}
433
434// Créer une instance du client API
435const api = new JurassicAPIClient({
436 baseUrl: 'https://api.jurassic-park.com'
437});
438
439// Exemples d'utilisation du client API
440
441// Connexion
442async function loginUser(email, password) {
443 const result = await api.login(email, password);
444 if (result.success) {
445 console.log(`Connecté en tant que ${result.user.name}`);
446 return true;
447 } else {
448 console.error(`Erreur de connexion : ${result.error}`);
449 return false;
450 }
451}
452
453// Récupérer les données d'un dinosaure
454async function getDinosaur(id) {
455 try {
456 return await api.get(`/dinosaurs/${id}`);
457 } catch (error) {
458 console.error(`Échec de la récupération du dinosaure #${id} :`, error.message);
459 throw error;
460 }
461}
462
463// Mettre à jour les données d'un dinosaure
464async function updateDinosaur(id, data) {
465 try {
466 return await api.put(`/dinosaurs/${id}`, data);
467 } catch (error) {
468 console.error(`Échec de la mise à jour du dinosaure #${id} :`, error.message);
469 throw error;
470 }
471}
472
473// Récupérer la liste des dinosaures avec filtrage
474async function getDinosaurList(filter = {}) {
475 try {
476 return await api.get('/dinosaurs', {
477 body: filter,
478 cacheTime: 60000 // Cache plus court pour les listes (1 minute)
479 });
480 } catch (error) {
481 console.error('Échec de la récupération de la liste des dinosaures :', error.message);
482 throw error;
483 }
484}
485
486// Téléverser les dossiers médicaux d'un dinosaure
487async function sendMedicalRecords(dinoId, file, data) {
488 const progressBar = document.getElementById('upload-progress');
489 const statusText = document.getElementById('upload-status');
490
491 try {
492 return await api.uploadFile(`/dinosaurs/${dinoId}/medical-records`, file, {
493 additionalData: data,
494 onProgress: (info) => {
495 if (progressBar) progressBar.value = info.percent;
496 if (statusText) statusText.textContent = `Téléversement : ${info.percent}%`;
497 }
498 });
499 } catch (error) {
500 console.error(`Échec du téléversement de la documentation pour le dinosaure #${dinoId} :`, error.message);
501 if (statusText) statusText.textContent = `Erreur : ${error.message}`;
502 throw error;
503 }
504}Dans cet exemple complet, nous avons implémenté un client API avancé qui gère :
- Une gestion complète des erreurs - différents types d'erreurs HTTP et réseau
- Des nouvelles tentatives automatiques des requêtes - pour les problèmes temporaires
- La mise en cache des réponses - pour un accès plus rapide et le support du mode hors ligne
- La mise en file d'attente des opérations hors ligne - pour garantir la cohérence des données
- La surveillance de l'état du réseau - pour prendre les bonnes décisions
- L'authentification et le rafraîchissement du token - pour un accès sécurisé à l'API
- Le téléversement de fichiers avec suivi de progression - pour une meilleure expérience utilisateur
Un tel client API complet offre une résilience face aux problèmes réseau, une efficacité grâce à la mise en cache, le support du mode hors ligne, et des mécanismes conviviaux comme le suivi de progression. C'est une solution idéale pour des systèmes critiques comme le système de gestion du Parc Jurassique, où la fiabilité et la sécurité sont primordiales.
Dans la prochaine leçon, nous verrons comment traiter efficacement les données JSON reçues de l'API pour les présenter aux utilisateurs de façon accessible.
Code de cette leçon : index.js
1// Function Factory - Fabrique de fonctions
2console.log("FUNCTION FACTORY - FABRIQUE DE FONCTIONS\n");
3
4// Factory function - a function that creates other functions
5function createDinosaurHandler(species, defaultDangerLevel) {
6 console.log(`Création du handler pour: ${species}`);
7
8 // Nous retournons a new function with "remembered" parameters
9 return function(action) {
10 console.log(`\n[${species}] Action: ${action}`);
11 console.log(` Default danger: ${defaultDangerLevel}/10`);
12
13 switch(action) {
14 case 'FEED':
15 console.log(` Feeding ${species}`);
16 break;
17 case 'MONITOR':
18 console.log(` Monitoring ${species}`);
19 break;
20 case 'ALERT':
21 console.log(` ALARM! ${species} needs attention!`);
22 break;
23 default:
24 console.log(` Action inconnue`);
25 }
26 };
27}
28
29// Creating specialized handlers
30console.log("=== CREATING HANDLERS ===\n");
31
32const trexHandler = createDinosaurHandler('T-Rex', 10);
33const triceratopsHandler = createDinosaurHandler('Triceratops', 3);
34const velociraptorHandler = createDinosaurHandler('Velociraptor', 8);
35
36// Using handlers
37console.log("\n=== USING HANDLERS ===");
38
39trexHandler('FEED');
40trexHandler('MONITOR');
41
42triceratopsHandler('FEED');
43velociraptorHandler('ALERT');
44
45// Factory avec options supplémentaires
46console.log("\n=== FACTORY AVANCÉE ===\n");
47
48function createAdvancedHandler(config) {
49 const {
50 species,
51 dangerLevel,
52 feedingTime,
53 maxCapacity
54 } = config;
55
56 let currentCount = 0;
57
58 return {
59 add: function() {
60 if (currentCount < maxCapacity) {
61 currentCount++;
62 console.log(`Added ${species}. Currently: ${currentCount}/${maxCapacity}`);
63 return true;
64 } else {
65 console.log(`Pas de place pour ${species} ! (${currentCount}/${maxCapacity})`);
66 return false;
67 }
68 },
69
70 remove: function() {
71 if (currentCount > 0) {
72 currentCount--;
73 console.log(`Removed ${species}. Currently: ${currentCount}/${maxCapacity}`);
74 return true;
75 } else {
76 console.log(`Aucun ${species} à retirer`);
77 return false;
78 }
79 },
80
81 getInfo: function() {
82 return {
83 species,
84 dangerLevel,
85 feedingTime,
86 count: currentCount,
87 capacity: maxCapacity,
88 occupancy: `${((currentCount / maxCapacity) * 100).toFixed(1)}%`
89 };
90 },
91
92 feed: function() {
93 console.log(`Feeding ${currentCount} x ${species} à ${feedingTime}`);
94 }
95 };
96}
97
98// Creating advanced handlers
99const trexManager = createAdvancedHandler({
100 species: 'T-Rex',
101 dangerLevel: 10,
102 feedingTime: '18:00',
103 maxCapacity: 5
104});
105
106const velociraptorManager = createAdvancedHandler({
107 species: 'Velociraptor',
108 dangerLevel: 8,
109 feedingTime: '16:00',
110 maxCapacity: 10
111});
112
113console.log("=== POPULATION MANAGEMENT ===\n");
114
115trexManager.add();
116trexManager.add();
117trexManager.add();
118
119console.log("\nT-Rex Info:", trexManager.getInfo());
120
121trexManager.feed();
122
123console.log("");
124
125velociraptorManager.add();
126velociraptorManager.add();
127velociraptorManager.add();
128velociraptorManager.add();
129velociraptorManager.add();
130
131console.log("\nVelociraptor Info:", velociraptorManager.getInfo());
132
133console.log("\n=== BENEFITS OF FACTORY PATTERN ===");
134console.log("Encapsulation - hidden internal data");
135console.log("Reusability - easy creation of similar objects");
136console.log("Personnalisation - each handler can have its own configuration");
137console.log("Closures - each handler remembers its settings");Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que le pattern retry dans le contexte des requêtes HTTP ?
2. Quelle technique est la plus efficace pour déboguer des erreurs non gérées dans une Promise ?
Tâches pratiques dans le jeu
- Éditeur de code
Écris une fonction async fetchWithRetry(url, maxRetries) qui relance fetch après une erreur avec un délai croissant.
- Tri vertical
Ordonne les étapes du pattern retry avec exponential backoff :
- Clique dans l'ordre
Ordonne les éléments d'une course de Promises (race) :
- Tri vertical
Ordonne l'ordre d'exécution de ce code : console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');
- Tri vertical
Ordonne l'ordre dans lequel l'Event Loop traite le code :
- Tri vertical
Ordonne les étapes de récupération de données depuis une API :