W poprzednim ćwiczeniu zapoznaliśmy się z podstawami Fetch API, które pozwala na wykonywanie żądań HTTP w JavaScript. Teraz rozszerzymy naszą wiedzę o bardziej zaawansowane techniki i praktyczne wzorce, które pomogą nam stworzyć solidny system komunikacji w naszym Parku Jurajskim. Przyjrzymy się, jak obsługiwać różne scenariusze, które mogą wystąpić podczas komunikacji sieciowej.
W środowisku tak krytycznym jak Park Jurajski, gdzie awaria systemu może doprowadzić do niebezpiecznych sytuacji, prawidłowa obsługa błędów w komunikacji HTTP jest niezwykle ważna. Stwórzmy bardziej zaawansowany mechanizm obsługi błędów:
1async function bezpieczneZapytanie(url, options = {}) {
2 const controller = new AbortController();
3 const { signal } = controller;
4
5 // Ustawienie timeoutu (10 sekund)
6 const timeoutId = setTimeout(() => controller.abort(), 10000);
7
8 try {
9 const response = await fetch(url, { ...options, signal });
10 clearTimeout(timeoutId);
11
12 // Kategorie błędów HTTP
13 if (response.status >= 500) {
14 throw new Error('Błąd serwera, prawdopodobnie tymczasowy problem.');
15 } else if (response.status === 404) {
16 throw new Error('Zasób nie został znaleziony.');
17 } else if (response.status === 403) {
18 throw new Error('Brak uprawnień do tego zasobu.');
19 } else if (response.status === 401) {
20 throw new Error('Wymagane uwierzytelnienie. Zaloguj się ponownie.');
21 } else if (!response.ok) {
22 throw new Error(`Nieoczekiwany błąd HTTP: ${response.status}`);
23 }
24
25 // Obsługa różnych typów odpowiedzi
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 // Obsługa różnych typów błędów
38 if (error.name === 'AbortError') {
39 throw new Error('Żądanie przerwane - upłynął limit czasu (10s).');
40 } else if (!navigator.onLine) {
41 throw new Error('Brak połączenia internetowego. Sprawdź swoje połączenie z siecią.');
42 } else {
43 console.error('Szczegóły błędu:', error);
44 throw new Error(`Błąd podczas komunikacji z serwerem: ${error.message}`);
45 }
46 }
47}W sieci Parku Jurajskiego mogą występować chwilowe problemy z łącznością, szczególnie w odległych częściach wyspy. Zaimplementujmy mechanizm automatycznego ponawiania nieudanych żądań:
1/**
2 * Wykonuje żądanie HTTP z automatycznymi ponownymi próbami
3 * @param {Function} requestFn - Funkcja wykonująca żądanie, zwracająca Promise
4 * @param {Object} options - Opcje konfiguracyjne
5 * @returns {Promise} - Wynik żądania lub ostatni błąd po wyczerpaniu prób
6 */
7async function zPonawianiem(requestFn, options = {}) {
8 const {
9 maxProb = 3,
10 poczatkoweOpoznienie = 1000,
11 czynnikOpoznienia = 2,
12 maxOpoznienie = 10000,
13 timeoutMs = 10000,
14 czyPonowic = (error) => {
15 // Domyślnie ponawiamy tylko dla błędów serwera (5xx) i problemów z siecią
16 return (
17 error.message.includes('Błąd serwera') ||
18 error.message.includes('Brak połączenia') ||
19 error.name === 'AbortError'
20 );
21 }
22 } = options;
23
24 let ostatniBlad;
25 let proba = 0;
26
27 while (proba <= maxProb) {
28 try {
29 // Dla każdej próby tworzymy nowy kontroler przerwania
30 const controller = new AbortController();
31 const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
32
33 // Jeśli to nie pierwsza próba, czekamy przed ponowieniem
34 if (proba > 0) {
35 const opoznienie = Math.min(
36 poczatkoweOpoznienie * Math.pow(czynnikOpoznienia, proba - 1),
37 maxOpoznienie
38 );
39 console.log(`Próba ${proba}/${maxProb} nie powiodła się. Ponowienie za ${opoznienie}ms...`);
40 await new Promise(resolve => setTimeout(resolve, opoznienie));
41 }
42
43 // Wykonanie żądania
44 const wynik = await requestFn(controller.signal);
45 clearTimeout(timeoutId);
46 return wynik;
47 } catch (error) {
48 clearTimeout(timeoutId);
49 ostatniBlad = error;
50
51 // Sprawdzenie, czy ponawiać próbę
52 if (proba === maxProb || !czyPonowic(error)) {
53 break;
54 }
55
56 proba++;
57 }
58 }
59
60 // Wyrzucenie ostatniego błędu po wyczerpaniu prób
61 throw new Error(`Nie udało się po ${maxProb} próbach: ${ostatniBlad.message}`);
62}
63
64// Przykład użycia
65async function pobierzDaneDinozaura(id) {
66 return zPonawianiem(
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(`Błąd HTTP: ${response.status}`);
72 }
73
74 return response.json();
75 },
76 {
77 maxProb: 5,
78 poczatkoweOpoznienie: 500,
79 czyPonowic: (error) => error.message.includes('Błąd HTTP: 5')
80 }
81 );
82}W sytuacjach, gdy musimy wykonać wiele żądań HTTP, ale chcemy ograniczyć liczbę równoczesnych połączeń, aby nie przeciążyć sieci lub serwera, możemy zaimplementować kolejkowanie żądań:
1class KolejkaZadan {
2 constructor(maxRownoleglosc = 3) {
3 this.maxRownoleglosc = maxRownoleglosc;
4 this.aktywneZadania = 0;
5 this.kolejka = [];
6 }
7
8 async dodaj(zadanie) {
9 // Jeśli mamy miejsce na więcej równoległych zadań, wykonaj od razu
10 if (this.aktywneZadania < this.maxRownoleglosc) {
11 return this.wykonaj(zadanie);
12 }
13
14 // W przeciwnym razie dodaj do kolejki
15 return new Promise((resolve, reject) => {
16 this.kolejka.push({ zadanie, resolve, reject });
17 });
18 }
19
20 async wykonaj(zadanie) {
21 this.aktywneZadania++;
22
23 try {
24 // Wykonanie zadania
25 const wynik = await zadanie();
26 return wynik;
27 } catch (error) {
28 throw error;
29 } finally {
30 this.aktywneZadania--;
31 this.przetworzNastepne();
32 }
33 }
34
35 przetworzNastepne() {
36 if (this.kolejka.length === 0 || this.aktywneZadania >= this.maxRownoleglosc) {
37 return;
38 }
39
40 // Pobierz następne zadanie z kolejki
41 const { zadanie, resolve, reject } = this.kolejka.shift();
42
43 // Wykonaj zadanie i przekieruj wynik do oczekującego Promise
44 this.wykonaj(zadanie)
45 .then(resolve)
46 .catch(reject);
47 }
48}
49
50// Przykład użycia kolejki do monitorowania wielu dinozaurów
51async function monitorujWszystkieDinozaury(listaId) {
52 const kolejka = new KolejkaZadan(5); // Maksymalnie 5 równoczesnych żądań
53 const wyniki = [];
54 const bledy = [];
55
56 // Funkcje pomocnicze do śledzenia postępu
57 const totalZadan = listaId.length;
58 let ukonczoneZadania = 0;
59
60 function aktualizujPostep() {
61 ukonczoneZadania++;
62 const procent = Math.floor((ukonczoneZadania / totalZadan) * 100);
63 console.log(`Postęp monitorowania: ${procent}% (${ukonczoneZadania}/${totalZadan})`);
64 }
65
66 // Tworzenie tablicy Promise dla wszystkich zadań
67 const promisy = listaId.map(id => {
68 return kolejka.dodaj(async () => {
69 try {
70 console.log(`Monitorowanie dinozaura #${id}...`);
71 const wynik = await pobierzDaneDinozaura(id);
72 wyniki.push(wynik);
73 aktualizujPostep();
74 return wynik;
75 } catch (error) {
76 bledy.push({ id, error: error.message });
77 aktualizujPostep();
78 throw error;
79 }
80 }).catch(error => {
81 // Przechwytujemy błędy, aby nie przerywać całego procesu
82 console.error(`Błąd monitorowania dinozaura #${id}:`, error.message);
83 });
84 });
85
86 // Czekamy na zakończenie wszystkich zadań
87 await Promise.all(promisy);
88
89 return {
90 wyniki,
91 bledy,
92 podsumowanie: {
93 total: totalZadan,
94 sukces: wyniki.length,
95 blad: bledy.length
96 }
97 };
98}W Parku Jurajskim mamy dane, które zmieniają się rzadko (np. gatunki dinozaurów) oraz dane, które zmieniają się często (np. lokalizacje dinozaurów). Implementacja pamięci podręcznej pomoże zoptymalizować komunikację:
1class ZarzadcaPamieciPodrecznej {
2 constructor() {
3 this.cache = new Map();
4 this.statystyki = {
5 trafienia: 0,
6 pudla: 0,
7 unieważnienia: 0
8 };
9 }
10
11 /**
12 * Pobiera dane z cache lub wykonuje funkcję pobierającą, jeśli danych nie ma w cache
13 * @param {string} klucz - Unikalny identyfikator danych w cache
14 * @param {Function} pobierzFn - Funkcja pobierająca dane, jeśli nie ma ich w cache
15 * @param {Object} opcje - Opcje cache
16 * @returns {Promise} - Promise z danymi
17 */
18 async pobierz(klucz, pobierzFn, opcje = {}) {
19 const {
20 ttl = 60000, // Domyślny czas życia: 1 minuta
21 forceRefresh = false, // Czy wymusić odświeżenie pomimo obecności w cache
22 onlyIfCached = false // Czy zwracać tylko z cache (bez zapytania sieciowego)
23 } = opcje;
24
25 // Sprawdź czy dane są w cache i czy są aktualne
26 const wpis = this.cache.get(klucz);
27 const teraz = Date.now();
28
29 if (wpis && !forceRefresh && teraz < wpis.expiresAt) {
30 // Dane znalezione w cache i ważne
31 this.statystyki.trafienia++;
32 return wpis.dane;
33 }
34
35 // Jeśli żądamy tylko z cache i nie ma danych, zwróć null
36 if (onlyIfCached) {
37 return null;
38 }
39
40 // Danych nie ma w cache lub wymuszamy odświeżenie
41 this.statystyki.pudla++;
42
43 try {
44 // Pobierz dane
45 const dane = await pobierzFn();
46
47 // Zapisz w cache
48 this.cache.set(klucz, {
49 dane,
50 expiresAt: teraz + ttl,
51 dodanoAt: teraz
52 });
53
54 return dane;
55 } catch (error) {
56 // Jeśli mamy stare dane w cache, możemy je zwrócić jako awaryjne
57 if (wpis) {
58 console.warn(`Błąd pobierania danych dla ${klucz}, używam przestarzałych danych z cache:`, error);
59 return wpis.dane;
60 }
61
62 // W przeciwnym razie propagujemy błąd
63 throw error;
64 }
65 }
66
67 /**
68 * Unieważnia wpisy w cache pasujące do wzorca
69 * @param {string|RegExp} wzorzec - Wzorzec do dopasowania kluczy cache
70 */
71 uniewaznij(wzorzec) {
72 const isString = typeof wzorzec === 'string';
73 let licznik = 0;
74
75 for (const klucz of this.cache.keys()) {
76 if (isString ? klucz.includes(wzorzec) : wzorzec.test(klucz)) {
77 this.cache.delete(klucz);
78 licznik++;
79 }
80 }
81
82 this.statystyki.unieważnienia += licznik;
83 return licznik;
84 }
85
86 /**
87 * Czyści wszystkie wpisy cache
88 */
89 wyczysc() {
90 const licznik = this.cache.size;
91 this.cache.clear();
92 this.statystyki.unieważnienia += licznik;
93 return licznik;
94 }
95
96 /**
97 * Zwraca statystyki użycia cache
98 */
99 pobierzStatystyki() {
100 const totalZapytan = this.statystyki.trafienia + this.statystyki.pudla;
101 const wspolczynnikTrafien = totalZapytan === 0 ? 0 : this.statystyki.trafienia / totalZapytan;
102
103 return {
104 ...this.statystyki,
105 totalZapytan,
106 wspolczynnikTrafien: wspolczynnikTrafien.toFixed(2),
107 aktywneWpisy: this.cache.size
108 };
109 }
110}
111
112// Utworzenie instancji zarządcy pamięci podręcznej
113const cacheDinozaurow = new ZarzadcaPamieciPodrecznej();
114
115// Przykład użycia
116async function pobierzDaneDinozauraZCache(id, opcje = {}) {
117 return cacheDinozaurow.pobierz(
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(`Błąd HTTP: ${response.status}`);
123 }
124 return response.json();
125 },
126 {
127 ttl: 300000, // 5 minut dla danych dinozaura
128 ...opcje
129 }
130 );
131}
132
133// Funkcja do unieważniania cache po aktualizacji
134async function aktualizujDinozaura(id, dane) {
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(dane)
139 });
140
141 if (!response.ok) {
142 throw new Error(`Błąd aktualizacji: ${response.status}`);
143 }
144
145 // Unieważnij cache tego dinozaura
146 cacheDinozaurow.uniewaznij(`dino-${id}`);
147
148 // Unieważnij również listę dinozaurów, które mogą zawierać tego dinozaura
149 cacheDinozaurow.uniewaznij('dino-list');
150
151 return response.json();
152}
153
154// Pobieranie listy dinozaurów z cache na krócej (dane zmieniają się częściej)
155async function pobierzListeDinozaurow(filter = {}) {
156 const queryParams = new URLSearchParams(filter).toString();
157 const cacheKey = `dino-list-${queryParams || 'all'}`;
158
159 return cacheDinozaurow.pobierz(
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(`Błąd HTTP: ${response.status}`);
166 }
167 return response.json();
168 },
169 { ttl: 60000 } // 1 minuta dla list dinozaurów
170 );
171}W Parku Jurajskim musimy czasami wykonywać operacje, które wymagają synchronizacji z serwerem, np. aktualizacje statusu zdrowia dinozaurów. Zaimplementujmy mechanizm, który zapewni, że dane zostaną zapisane nawet w przypadku problemów z łącznością:
1class KolejkaSynchronizacji {
2 constructor(opcje = {}) {
3 // Klucz w localStorage do przechowywania kolejki
4 this.klucz = opcje.klucz || 'dinozaury-sync-queue';
5
6 // Ładowanie zapisanej kolejki
7 this.kolejka = this.zaladujKolejke();
8
9 // Flaga wskazująca, czy synchronizacja jest aktywna
10 this.jestSynchronizacja = false;
11
12 // Automatyczne rozpoczęcie synchronizacji
13 if (opcje.autoSync !== false) {
14 this.rozpocznijSynchronizacje();
15 }
16
17 // Nasłuchiwanie na zmiany stanu online/offline
18 window.addEventListener('online', () => {
19 console.log('Połączenie przywrócone. Rozpoczynam synchronizację...');
20 this.rozpocznijSynchronizacje();
21 });
22
23 window.addEventListener('offline', () => {
24 console.log('Utracono połączenie. Synchronizacja wstrzymana.');
25 });
26 }
27
28 /**
29 * Ładuje kolejkę z localStorage
30 */
31 zaladujKolejke() {
32 try {
33 const zapisanaKolejka = localStorage.getItem(this.klucz);
34 return zapisanaKolejka ? JSON.parse(zapisanaKolejka) : [];
35 } catch (error) {
36 console.error('Błąd podczas ładowania kolejki synchronizacji:', error);
37 return [];
38 }
39 }
40
41 /**
42 * Zapisuje kolejkę do localStorage
43 */
44 zapiszKolejke() {
45 try {
46 localStorage.setItem(this.klucz, JSON.stringify(this.kolejka));
47 } catch (error) {
48 console.error('Błąd podczas zapisywania kolejki synchronizacji:', error);
49 }
50 }
51
52 /**
53 * Dodaje operację do kolejki synchronizacji
54 * @param {string} url - URL do żądania
55 * @param {Object} opcje - Opcje fetch
56 * @param {string} typ - Typ operacji (np. 'aktualizacja-zdrowia')
57 * @returns {string} - ID operacji w kolejce
58 */
59 dodajDoKolejki(url, opcje = {}, typ = 'nieznany') {
60 const id = `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
61
62 this.kolejka.push({
63 id,
64 url,
65 opcje,
66 typ,
67 dodano: new Date().toISOString(),
68 proby: 0
69 });
70
71 this.zapiszKolejke();
72 this.rozpocznijSynchronizacje();
73
74 return id;
75 }
76
77 /**
78 * Rozpoczyna proces synchronizacji
79 */
80 async rozpocznijSynchronizacje() {
81 // Jeśli już synchronizujemy lub nie ma połączenia, nie rób nic
82 if (this.jestSynchronizacja || !navigator.onLine || this.kolejka.length === 0) {
83 return;
84 }
85
86 this.jestSynchronizacja = true;
87
88 try {
89 let index = 0;
90 while (index < this.kolejka.length) {
91 const operacja = this.kolejka[index];
92
93 try {
94 console.log(`Synchronizacja operacji: ${operacja.typ} (${operacja.id})`);
95
96 // Inkrementacja liczby prób
97 operacja.proby++;
98 this.zapiszKolejke();
99
100 // Wykonanie żądania
101 const response = await fetch(operacja.url, operacja.opcje);
102
103 if (!response.ok) {
104 throw new Error(`Błąd HTTP: ${response.status}`);
105 }
106
107 // Operacja zakończona sukcesem, usuń z kolejki
108 this.kolejka.splice(index, 1);
109 this.zapiszKolejke();
110
111 console.log(`Operacja ${operacja.id} zsynchronizowana pomyślnie`);
112
113 // Nie zwiększaj indeksu, ponieważ usunęliśmy element
114 } catch (error) {
115 console.error(`Błąd synchronizacji operacji ${operacja.id}:`, error);
116
117 // Jeśli osiągnęliśmy maksymalną liczbę prób, oznacz jako nieudaną
118 if (operacja.proby >= 5) {
119 console.warn(`Operacja ${operacja.id} przekroczyła limit prób. Oznaczanie jako nieudana.`);
120 operacja.status = 'nieudana';
121 operacja.bladKomunikat = error.message;
122 this.zapiszKolejke();
123 }
124
125 // Przejdź do następnej operacji
126 index++;
127 }
128 }
129 } finally {
130 this.jestSynchronizacja = false;
131
132 // Jeśli wciąż są elementy w kolejce, ponowna próba za 1 minutę
133 if (this.kolejka.some(op => !op.status || op.status !== 'nieudana')) {
134 setTimeout(() => this.rozpocznijSynchronizacje(), 60000);
135 }
136 }
137 }
138
139 /**
140 * Usuwa nieudane operacje z kolejki
141 */
142 wyczyscNieudaneOperacje() {
143 const poprzedniaLiczba = this.kolejka.length;
144 this.kolejka = this.kolejka.filter(op => !op.status || op.status !== 'nieudana');
145 this.zapiszKolejke();
146 return poprzedniaLiczba - this.kolejka.length;
147 }
148
149 /**
150 * Zwraca aktualny stan kolejki
151 */
152 pobierzStatus() {
153 const wszystkie = this.kolejka.length;
154 const nieudane = this.kolejka.filter(op => op.status === 'nieudana').length;
155 const oczekujace = wszystkie - nieudane;
156
157 return {
158 wszystkie,
159 oczekujace,
160 nieudane,
161 jestSynchronizacja: this.jestSynchronizacja
162 };
163 }
164}
165
166// Utworzenie instancji kolejki synchronizacji
167const syncDinozaurow = new KolejkaSynchronizacji({
168 klucz: 'jurassic-park-dino-sync'
169});
170
171// Przykład użycia: aktualizacja zdrowia dinozaura, która będzie synchronizowana
172function aktualizujZdrowieDinozaura(id, parametryZdrowia) {
173 // Przygotuj dane do wysyłki
174 const dane = {
175 id,
176 health: parametryZdrowia.health,
177 lastCheckTime: new Date().toISOString(),
178 symptoms: parametryZdrowia.symptoms || [],
179 updatedBy: 'dr-wu'
180 };
181
182 // Jeśli jesteśmy online, próbuj od razu
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(dane)
188 })
189 .then(response => {
190 if (!response.ok) {
191 throw new Error(`Błąd HTTP: ${response.status}`);
192 }
193 return response.json();
194 })
195 .catch(error => {
196 console.warn('Nie udało się zaktualizować zdrowia online, dodaję do kolejki synchronizacji');
197
198 // Dodaj do kolejki synchronizacji
199 syncDinozaurow.dodajDoKolejki(
200 `https://api.jurassic-park.com/dinosaurs/${id}/health`,
201 {
202 method: 'PUT',
203 headers: { 'Content-Type': 'application/json' },
204 body: JSON.stringify(dane)
205 },
206 `aktualizacja-zdrowia-${id}`
207 );
208
209 // Zwróć dane, jakby operacja się powiodła, choć będzie zsynchronizowana później
210 return {
211 ...dane,
212 pendingSync: true
213 };
214 });
215 } else {
216 // Jeśli jesteśmy offline, od razu dodaj do kolejki
217 syncDinozaurow.dodajDoKolejki(
218 `https://api.jurassic-park.com/dinosaurs/${id}/health`,
219 {
220 method: 'PUT',
221 headers: { 'Content-Type': 'application/json' },
222 body: JSON.stringify(dane)
223 },
224 `aktualizacja-zdrowia-${id}`
225 );
226
227 return Promise.resolve({
228 ...dane,
229 pendingSync: true
230 });
231 }
232}W systemie Parku Jurajskiego potrzebujemy zabezpieczyć dostęp do API, używając tokenów uwierzytelniających. Zaimplementujmy mechanizm, który automatycznie odświeża token, gdy wygaśnie:
1class ZarzadcaUwierzytelniania {
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 * Pobiera aktualny token dostępu
12 * @param {boolean} autoRefresh - Czy automatycznie odświeżyć token, jeśli wygasł
13 * @returns {Promise<string>} - Promise z tokenem dostępu
14 */
15 async pobierzTokenDostepowy(autoRefresh = true) {
16 const token = localStorage.getItem(this.tokenAccessKey);
17 const expiry = localStorage.getItem(this.expiryKey);
18
19 // Sprawdź, czy token istnieje i czy nie wygasł
20 if (token && expiry) {
21 const expiryDate = new Date(expiry);
22 const now = new Date();
23
24 // Dodajemy 1 minutę buforu, aby uniknąć używania wygasającego za chwilę tokenu
25 const bufferTime = 60 * 1000; // 1 minuta w milisekundach
26
27 if (expiryDate.getTime() - now.getTime() > bufferTime) {
28 return token;
29 }
30
31 // Token wygasł lub wygaśnie wkrótce
32 if (autoRefresh) {
33 return this.odswiezToken();
34 }
35 }
36
37 throw new Error('Brak ważnego tokenu dostępowego');
38 }
39
40 /**
41 * Odświeża token dostępu używając tokenu odświeżającego
42 * @returns {Promise<string>} - Promise z nowym tokenem dostępu
43 */
44 async odswiezToken() {
45 // Jeśli odświeżanie jest już w toku, zaczekaj na jego wynik
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('Brak tokenu odświeżającego');
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 // Token odświeżający jest nieprawidłowy, wyloguj użytkownika
70 this.wyloguj();
71 throw new Error('Sesja wygasła. Zaloguj się ponownie.');
72 }
73 throw new Error(`Błąd odświeżania tokenu: ${response.status}`);
74 }
75
76 const data = await response.json();
77
78 // Zapisz nowe tokeny
79 localStorage.setItem(this.tokenAccessKey, data.access_token);
80 localStorage.setItem(this.tokenRefreshKey, data.refresh_token);
81
82 // Oblicz czas wygaśnięcia
83 const expiry = new Date();
84 expiry.setSeconds(expiry.getSeconds() + data.expires_in);
85 localStorage.setItem(this.expiryKey, expiry.toISOString());
86
87 // Powiadom wszystkich oczekujących
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 // W przypadku błędu, powiadom wszystkich oczekujących
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 * Zapisuje tokeny po zalogowaniu
109 * @param {Object} authData - Dane uwierzytelniające
110 */
111 zapiszTokeny(authData) {
112 localStorage.setItem(this.tokenAccessKey, authData.access_token);
113 localStorage.setItem(this.tokenRefreshKey, authData.refresh_token);
114
115 // Oblicz czas wygaśnięcia
116 const expiry = new Date();
117 expiry.setSeconds(expiry.getSeconds() + authData.expires_in);
118 localStorage.setItem(this.expiryKey, expiry.toISOString());
119 }
120
121 /**
122 * Wylogowuje użytkownika
123 */
124 wyloguj() {
125 localStorage.removeItem(this.tokenAccessKey);
126 localStorage.removeItem(this.tokenRefreshKey);
127 localStorage.removeItem(this.expiryKey);
128 }
129
130 /**
131 * Sprawdza, czy użytkownik jest zalogowany
132 * @returns {boolean}
133 */
134 czyZalogowany() {
135 return !!localStorage.getItem(this.tokenAccessKey);
136 }
137}
138
139// Tworzenie instancji zarządcy uwierzytelniania
140const auth = new ZarzadcaUwierzytelniania();
141
142// Przykład użycia - funkcja do wykonywania uwierzytelnionych żądań
143async function zapytanieUwierzytelnione(url, options = {}) {
144 try {
145 // Pobierz token dostępu (z automatycznym odświeżaniem)
146 const token = await auth.pobierzTokenDostepowy();
147
148 // Dodaj token do nagłówków
149 const authOptions = {
150 ...options,
151 headers: {
152 ...options.headers,
153 'Authorization': `Bearer ${token}`
154 }
155 };
156
157 // Wykonaj żądanie
158 const response = await fetch(url, authOptions);
159
160 // Obsługa błędów autoryzacji
161 if (response.status === 401) {
162 // Spróbuj odświeżyć token i ponowić żądanie
163 try {
164 const newToken = await auth.odswiezToken();
165
166 // Ponów żądanie z nowym tokenem
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 // Nie udało się odświeżyć tokenu, wyloguj użytkownika
178 auth.wyloguj();
179 window.location.href = '/login'; // Przekieruj do logowania
180 throw new Error('Sesja wygasła. Zaloguj się ponownie.');
181 }
182 }
183
184 return response;
185 } catch (error) {
186 console.error('Błąd uwierzytelnionego żądania:', error);
187 throw error;
188 }
189}
190
191// Przykład użycia
192async function pobierzDaneWybiegu(wybiegId) {
193 const response = await zapytanieUwierzytelnione(
194 `https://api.jurassic-park.com/paddocks/${wybiegId}`
195 );
196
197 if (!response.ok) {
198 throw new Error(`Błąd pobierania danych wybiegu: ${response.status}`);
199 }
200
201 return response.json();
202}
203
204// Funkcja logowania
205async function zaloguj(email, haslo) {
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: haslo })
211 });
212
213 if (!response.ok) {
214 throw new Error(`Błąd logowania: ${response.status}`);
215 }
216
217 const authData = await response.json();
218
219 // Zapisz tokeny
220 auth.zapiszTokeny(authData);
221
222 return {
223 success: true,
224 user: authData.user
225 };
226 } catch (error) {
227 console.error('Błąd logowania:', error);
228 return {
229 success: false,
230 error: error.message
231 };
232 }
233}W Parku Jurajskim często musimy przesyłać duże pliki, takie jak obrazy czy filmy z kamer monitoringu dinozaurów. Zaimplementujmy funkcję, która obsługuje przesyłanie plików z informacją o postępie:
1/**
2 * Wysyła plik na serwer z monitorowaniem postępu
3 * @param {File} plik - Plik do wysłania
4 * @param {string} url - URL docelowy
5 * @param {Object} opcje - Dodatkowe opcje
6 * @returns {Promise} - Promise z wynikiem operacji
7 */
8async function wyslijPlik(plik, url, opcje = {}) {
9 return new Promise((resolve, reject) => {
10 // Domyślne opcje
11 const {
12 metoda = 'POST',
13 nazwaPolaPliku = 'file',
14 dodatkoweDane = {},
15 naPostep = null,
16 naBlad = null,
17 maxRetries = 3
18 } = opcje;
19
20 // Licznik ponownych prób
21 let proby = 0;
22
23 // Funkcja do wykonania żądania
24 function wykonajWysylanie() {
25 // Utworzenie obiektu XHR
26 const xhr = new XMLHttpRequest();
27
28 // Konfiguracja obsługi zdarzeń
29 xhr.upload.addEventListener('progress', (event) => {
30 if (event.lengthComputable && typeof naPostep === 'function') {
31 const procent = Math.round((event.loaded / event.total) * 100);
32 naPostep({
33 loaded: event.loaded,
34 total: event.total,
35 procent
36 });
37 }
38 });
39
40 xhr.addEventListener('load', () => {
41 if (xhr.status >= 200 && xhr.status < 300) {
42 try {
43 // Parsowanie odpowiedzi JSON
44 const odpowiedz = JSON.parse(xhr.responseText);
45 resolve(odpowiedz);
46 } catch (e) {
47 resolve({
48 success: true,
49 status: xhr.status,
50 message: xhr.statusText
51 });
52 }
53 } else {
54 const blad = new Error(`Błąd HTTP: ${xhr.status}`);
55 blad.status = xhr.status;
56 blad.statusText = xhr.statusText;
57
58 if (xhr.status >= 500 && proby < maxRetries) {
59 // Dla błędów serwera, spróbuj ponownie
60 proby++;
61 console.warn(`Błąd wysyłania pliku (${xhr.status}). Ponowna próba ${proby}/${maxRetries}...`);
62 setTimeout(wykonajWysylanie, 2000 * proby); // Zwiększanie czasu między próbami
63 } else {
64 if (typeof naBlad === 'function') {
65 naBlad(blad);
66 }
67 reject(blad);
68 }
69 }
70 });
71
72 xhr.addEventListener('error', () => {
73 const blad = new Error('Błąd sieci podczas wysyłania pliku');
74 if (typeof naBlad === 'function') {
75 naBlad(blad);
76 }
77
78 if (proby < maxRetries) {
79 proby++;
80 console.warn(`Błąd sieci podczas wysyłania. Ponowna próba ${proby}/${maxRetries}...`);
81 setTimeout(wykonajWysylanie, 2000 * proby);
82 } else {
83 reject(blad);
84 }
85 });
86
87 xhr.addEventListener('abort', () => {
88 const blad = new Error('Wysyłanie pliku zostało przerwane');
89 if (typeof naBlad === 'function') {
90 naBlad(blad);
91 }
92 reject(blad);
93 });
94
95 // Otwarcie połączenia
96 xhr.open(metoda, url, true);
97
98 // Przygotowanie danych FormData
99 const formData = new FormData();
100 formData.append(nazwaPolaPliku, plik);
101
102 // Dodanie dodatkowych danych
103 Object.entries(dodatkoweDane).forEach(([klucz, wartosc]) => {
104 formData.append(klucz, wartosc);
105 });
106
107 // Wysłanie żądania
108 xhr.send(formData);
109
110 // Zwrócenie obiektu z metodą przerwania
111 return {
112 przerwij: () => xhr.abort()
113 };
114 }
115
116 // Rozpoczęcie wysyłania
117 wykonajWysylanie();
118 });
119}
120
121// Przykład użycia
122async function wyslijNagranieZKamery(plik, lokalizacja) {
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 = 'Przygotowywanie...';
129
130 try {
131 const wynik = await wyslijPlik(
132 plik,
133 'https://api.jurassic-park.com/security/camera-footage',
134 {
135 dodatkoweDane: {
136 location: lokalizacja,
137 timestamp: new Date().toISOString(),
138 operator: 'dr-wu'
139 },
140 naPostep: (info) => {
141 progressBar.value = info.procent;
142 statusText.textContent = `Wysyłanie: ${info.procent}%`;
143 },
144 naBlad: (blad) => {
145 statusText.textContent = `Błąd: ${blad.message}`;
146 progressBar.classList.add('error');
147 }
148 }
149 );
150
151 statusText.textContent = 'Wysyłanie ukończone!';
152 progressBar.value = 100;
153
154 setTimeout(() => {
155 controller.classList.add('hidden');
156 }, 3000);
157
158 return wynik;
159 } catch (error) {
160 statusText.textContent = `Błąd: ${error.message}`;
161 progressBar.classList.add('error');
162 throw error;
163 }
164}W Parku Jurajskim musimy być przygotowani na problemy z łącznością, zwłaszcza w oddalonych częściach wyspy. Zaimplementujmy system monitorowania stanu sieci:
1class MonitorSieci {
2 constructor(opcje = {}) {
3 this.sluchacze = {
4 zmiana: [],
5 online: [],
6 offline: []
7 };
8
9 this.ostatniPing = null;
10 this.opoznienie = null;
11 this.status = navigator.onLine ? 'online' : 'offline';
12 this.adresEndpointu = opcje.endpointPing || 'https://api.jurassic-park.com/ping';
13 this.interwalPingu = opcje.interwalPingu || 30000; // 30 sekund
14 this.timeoutPingu = opcje.timeoutPingu || 5000; // 5 sekund
15
16 // Dodanie nasłuchiwaczy na zdarzenia online/offline
17 window.addEventListener('online', this.obslugaOnline.bind(this));
18 window.addEventListener('offline', this.obslugaOffline.bind(this));
19
20 // Start regularnego pingowania
21 if (opcje.autostartPing !== false) {
22 this.startPingowanie();
23 }
24 }
25
26 /**
27 * Obsługa zdarzenia przejścia do trybu online
28 */
29 obslugaOnline() {
30 const poprzedniStatus = this.status;
31 this.status = 'online';
32
33 // Powiadomienie słuchaczy
34 this.powiadomSluchaczy('online');
35
36 if (poprzedniStatus !== this.status) {
37 this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
38 }
39
40 // Wykonaj natychmiastowy ping dla sprawdzenia rzeczywistej łączności
41 this.ping();
42 }
43
44 /**
45 * Obsługa zdarzenia przejścia do trybu offline
46 */
47 obslugaOffline() {
48 const poprzedniStatus = this.status;
49 this.status = 'offline';
50 this.opoznienie = null;
51
52 // Powiadomienie słuchaczy
53 this.powiadomSluchaczy('offline');
54
55 if (poprzedniStatus !== this.status) {
56 this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
57 }
58 }
59
60 /**
61 * Ping serwera dla sprawdzenia rzeczywistej łączności
62 * @returns {Promise<number>} - Promise z czasem opóźnienia w ms
63 */
64 async ping() {
65 if (!navigator.onLine) {
66 this.opoznienie = null;
67 return Promise.reject(new Error('Przeglądarka jest w trybie offline'));
68 }
69
70 try {
71 const startTime = Date.now();
72
73 // Ustawienie timeout dla fetch
74 const controller = new AbortController();
75 const timeoutId = setTimeout(() => controller.abort(), this.timeoutPingu);
76
77 const response = await fetch(`${this.adresEndpointu}?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(`Błąd HTTP: ${response.status}`);
87 }
88
89 // Oblicz opóźnienie
90 const endTime = Date.now();
91 this.opoznienie = endTime - startTime;
92 this.ostatniPing = new Date();
93
94 // Aktualizacja statusu
95 const poprzedniStatus = this.status;
96 this.status = 'online';
97
98 if (poprzedniStatus !== this.status) {
99 this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
100 this.powiadomSluchaczy('online');
101 }
102
103 return this.opoznienie;
104 } catch (error) {
105 // Jeśli timeout lub błąd sieci, oznacz jako offline
106 if (error.name === 'AbortError' || error instanceof TypeError) {
107 const poprzedniStatus = this.status;
108 this.status = 'offline';
109 this.opoznienie = null;
110
111 if (poprzedniStatus !== this.status) {
112 this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
113 this.powiadomSluchaczy('offline');
114 }
115 }
116
117 throw error;
118 }
119 }
120
121 /**
122 * Rozpoczyna regularne pingowanie serwera
123 */
124 startPingowanie() {
125 // Zatrzymaj istniejący interwał, jeśli istnieje
126 this.stopPingowanie();
127
128 // Wykonaj pierwszy ping natychmiast
129 this.ping().catch(e => console.warn('Błąd początkowego pingu:', e));
130
131 // Ustaw interwał pingowania
132 this.pingInterval = setInterval(() => {
133 this.ping().catch(e => console.warn('Błąd pingowania:', e));
134 }, this.interwalPingu);
135 }
136
137 /**
138 * Zatrzymuje regularne pingowanie serwera
139 */
140 stopPingowanie() {
141 if (this.pingInterval) {
142 clearInterval(this.pingInterval);
143 this.pingInterval = null;
144 }
145 }
146
147 /**
148 * Dodaje słuchacza na zdarzenie
149 * @param {string} typZdarzenia - Typ zdarzenia ('zmiana', 'online', 'offline')
150 * @param {Function} callback - Funkcja callback
151 */
152 nasluchuj(typZdarzenia, callback) {
153 if (this.sluchacze[typZdarzenia]) {
154 this.sluchacze[typZdarzenia].push(callback);
155 }
156 return this; // Dla chain API
157 }
158
159 /**
160 * Usuwa słuchacza
161 * @param {string} typZdarzenia - Typ zdarzenia
162 * @param {Function} callback - Funkcja callback do usunięcia
163 */
164 przestanNasluchiwac(typZdarzenia, callback) {
165 if (this.sluchacze[typZdarzenia]) {
166 this.sluchacze[typZdarzenia] = this.sluchacze[typZdarzenia].filter(cb => cb !== callback);
167 }
168 return this;
169 }
170
171 /**
172 * Powiadamia słuchaczy o zdarzeniu
173 * @param {string} typZdarzenia - Typ zdarzenia
174 * @param {Object} dane - Dane zdarzenia
175 */
176 powiadomSluchaczy(typZdarzenia, dane = {}) {
177 if (this.sluchacze[typZdarzenia]) {
178 this.sluchacze[typZdarzenia].forEach(callback => {
179 try {
180 callback({
181 typ: typZdarzenia,
182 timestamp: new Date(),
183 status: this.status,
184 opoznienie: this.opoznienie,
185 ...dane
186 });
187 } catch (e) {
188 console.error(`Błąd w słuchaczu ${typZdarzenia}:`, e);
189 }
190 });
191 }
192 }
193
194 /**
195 * Zwraca aktualny stan sieci
196 */
197 pobierzStan() {
198 return {
199 status: this.status,
200 opoznienie: this.opoznienie,
201 ostatniPing: this.ostatniPing,
202 przegldarkaOnline: navigator.onLine
203 };
204 }
205}
206
207// Utworzenie instancji monitora sieci
208const monitorSieci = new MonitorSieci();
209
210// Przykład użycia
211monitorSieci.nasluchuj('zmiana', (info) => {
212 console.log(`Zmiana stanu sieci: ${info.poprzedni} -> ${info.status}`);
213
214 // Aktualizacja interfejsu użytkownika
215 const statusIndicator = document.getElementById('network-status');
216 if (statusIndicator) {
217 statusIndicator.className = `status-${info.status}`;
218 statusIndicator.textContent = info.status === 'online'
219 ? `Online (ping: ${info.opoznienie}ms)`
220 : 'Offline';
221 }
222});
223
224monitorSieci.nasluchuj('offline', () => {
225 // Pokaż powiadomienie użytkownikowi
226 pokazPowiadomienie('Utracono połączenie z siecią', 'error');
227
228 // Włącz tryb offline w aplikacji
229 app.wlaczTrybOffline();
230});
231
232monitorSieci.nasluchuj('online', (info) => {
233 pokazPowiadomienie(`Połączenie przywrócone (ping: ${info.opoznienie}ms)`, 'success');
234
235 // Synchronizuj dane z serwerem
236 syncDinozaurow.rozpocznijSynchronizacje();
237
238 // Wyłącz tryb offline
239 app.wylaczTrybOffline();
240});
241
242function pokazPowiadomienie(tekst, typ) {
243 const notification = document.createElement('div');
244 notification.className = `notification ${typ}`;
245 notification.textContent = tekst;
246
247 document.body.appendChild(notification);
248
249 // Usuń powiadomienie po 5 sekundach
250 setTimeout(() => {
251 notification.classList.add('fade-out');
252 setTimeout(() => {
253 document.body.removeChild(notification);
254 }, 500);
255 }, 5000);
256}Łącząc wszystkie omówione techniki, możemy stworzyć wysoko wydajny, odporny na błędy i przyjazny dla użytkownika system komunikacji z API. Oto przykład kompletnego rozwiązania:
1/**
2 * Kompleksowy klient API dla systemu Parku Jurajskiego
3 */
4class JurassicAPIClient {
5 constructor(opcje = {}) {
6 // Konfiguracja bazowego URL
7 this.baseUrl = opcje.baseUrl || 'https://api.jurassic-park.com';
8
9 // Inicjalizacja zarządcy uwierzytelniania
10 this.auth = new ZarzadcaUwierzytelniania();
11
12 // Inicjalizacja cache
13 this.cache = new ZarzadcaPamieciPodrecznej();
14
15 // Inicjalizacja kolejki synchronizacji
16 this.syncQueue = new KolejkaSynchronizacji({
17 klucz: 'jp-sync-queue',
18 autoSync: true
19 });
20
21 // Inicjalizacja monitora sieci
22 this.networkMonitor = new MonitorSieci({
23 endpointPing: `${this.baseUrl}/ping`,
24 interwalPingu: 30000
25 });
26
27 // Monitorowanie stanu sieci
28 this.networkMonitor.nasluchuj('online', () => {
29 this.syncQueue.rozpocznijSynchronizacje();
30 });
31
32 // Domyślne opcje żądań
33 this.defaultOptions = {
34 timeout: 30000,
35 retries: 3,
36 cacheTime: 300000, // 5 minut
37 useCache: true
38 };
39 }
40
41 /**
42 * Wykonuje żądanie HTTP
43 * @param {string} endpoint - Endpoint API
44 * @param {Object} opcje - Opcje żądania
45 * @returns {Promise} - Promise z wynikiem żądania
46 */
47 async request(endpoint, opcje = {}) {
48 // Połączenie domyślnych opcji z przekazanymi
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 } = opcje;
61
62 // Przygotowanie klucza cache
63 const cacheKey = `${method}:${endpoint}:${JSON.stringify(body || {})}`;
64
65 // Sprawdzenie, czy zapytanie jest w cache (tylko dla GET)
66 if (method === 'GET' && useCache && !forceRefresh) {
67 try {
68 const cachedData = await this.cache.pobierz(
69 cacheKey,
70 () => {}, // Pusta funkcja, bo nie chcemy jeszcze wykonywać żądania
71 { ttl: cacheTime, onlyIfCached: true }
72 );
73
74 if (cachedData) {
75 return cachedData;
76 }
77 } catch (error) {
78 // Ignoruj błędy cache
79 }
80 }
81
82 // Sprawdzenie stanu sieci
83 if (!this.networkMonitor.pobierzStan().status === 'online') {
84 // Jesteśmy offline, sprawdź czy mamy dane w cache
85 if (method === 'GET' && useCache && offlineFallback) {
86 try {
87 const cachedData = await this.cache.pobierz(
88 cacheKey,
89 () => {}, // Pusta funkcja, bo nie chcemy jeszcze wykonywać żądania
90 { ttl: Infinity, onlyIfCached: true, ignoreExpiry: true }
91 );
92
93 if (cachedData) {
94 // Informacje o trybie offline
95 return {
96 ...cachedData,
97 _meta: {
98 fromCache: true,
99 offlineMode: true,
100 timestamp: new Date()
101 }
102 };
103 }
104 } catch (error) {
105 // Ignoruj błędy cache
106 }
107 }
108
109 // Jeśli to żądanie modyfikujące dane, dodaj do kolejki synchronizacji
110 if (method !== 'GET' && offlineFallback) {
111 return this.addToSyncQueue(endpoint, { method, body, headers, useAuth });
112 }
113
114 throw new Error('Brak połączenia z siecią. Nie można wykonać żądania.');
115 }
116
117 // Funkcja wykonująca właściwe żądanie
118 const executeRequest = async (signal) => {
119 // Przygotowanie URL
120 const url = `${this.baseUrl}${endpoint}`;
121
122 // Przygotowanie opcji żądania
123 const requestOptions = {
124 method,
125 headers: {
126 'Content-Type': 'application/json',
127 ...headers
128 },
129 signal
130 };
131
132 // Dodanie ciała żądania, jeśli istnieje
133 if (body) {
134 requestOptions.body = JSON.stringify(body);
135 }
136
137 // Dodanie tokenu uwierzytelniającego, jeśli wymagane
138 if (useAuth) {
139 try {
140 const token = await this.auth.pobierzTokenDostepowy();
141 requestOptions.headers['Authorization'] = `Bearer ${token}`;
142 } catch (error) {
143 // Jeśli nie udało się pobrać tokenu, a żądanie wymaga uwierzytelnienia
144 throw new Error('Brak uwierzytelnienia. Zaloguj się ponownie.');
145 }
146 }
147
148 // Wykonanie żądania z timeoutem
149 const controller = new AbortController();
150 const { signal: abortSignal } = controller;
151 if (signal) {
152 // Jeśli przekazano signal z zewnątrz, łączymy go z naszym
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 // Obsługa błędów HTTP
164 if (response.status === 401 && useAuth) {
165 // Próba odświeżenia tokenu
166 try {
167 await this.auth.odswiezToken();
168 // Ponowna próba z nowym tokenem
169 return this.request(endpoint, opcje);
170 } catch (refreshError) {
171 throw new Error('Sesja wygasła. Zaloguj się ponownie.');
172 }
173 }
174
175 if (!response.ok) {
176 const error = new Error(`Błąd HTTP: ${response.status}`);
177 error.status = response.status;
178 error.statusText = response.statusText;
179
180 // Próba pobrania szczegółów błędu z odpowiedzi
181 try {
182 const errorData = await response.json();
183 error.data = errorData;
184 error.message = errorData.message || error.message;
185 } catch (e) {
186 // Ignoruj błędy parsowania
187 }
188
189 throw error;
190 }
191
192 // Parsowanie odpowiedzi w zależności od typu
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 // Zapisanie odpowiedzi w cache dla żądań GET
205 if (method === 'GET' && useCache) {
206 this.cache.pobierz(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(`Żądanie przerwane po upływie czasu (${timeout}ms)`);
215 }
216
217 throw error;
218 }
219 };
220
221 // Wykonanie żądania z ponawianiem prób
222 return zPonawianiem(
223 (signal) => executeRequest(signal),
224 {
225 maxProb: retries,
226 poczatkoweOpoznienie: 1000,
227 czynnikOpoznienia: 2,
228 timeoutMs: timeout
229 }
230 );
231 }
232
233 /**
234 * Dodaje żądanie do kolejki synchronizacji (tryb offline)
235 * @param {string} endpoint - Endpoint API
236 * @param {Object} opcje - Opcje żądania
237 * @returns {Promise} - Promise z tymczasowym wynikiem
238 */
239 addToSyncQueue(endpoint, opcje) {
240 const { method, body } = opcje;
241
242 // Generowanie unikalnego ID dla operacji
243 const operationId = `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
244
245 // Dodanie do kolejki synchronizacji
246 this.syncQueue.dodajDoKolejki(
247 `${this.baseUrl}${endpoint}`,
248 {
249 method,
250 headers: {
251 'Content-Type': 'application/json',
252 // Token będzie dodany podczas rzeczywistej synchronizacji
253 },
254 body: body ? JSON.stringify(body) : undefined
255 },
256 `${method}-${endpoint}`
257 );
258
259 // Zwrócenie tymczasowej odpowiedzi
260 return Promise.resolve({
261 _meta: {
262 pendingSync: true,
263 operationId,
264 timestamp: new Date(),
265 offlineMode: true
266 },
267 ...body // Zwróć przekazane dane jako tymczasowy wynik
268 });
269 }
270
271 // Metody pomocnicze dla różnych typów żądań
272
273 /**
274 * Wykonuje żądanie GET
275 * @param {string} endpoint - Endpoint API
276 * @param {Object} opcje - Opcje żądania
277 * @returns {Promise} - Promise z wynikiem żądania
278 */
279 async get(endpoint, opcje = {}) {
280 return this.request(endpoint, { ...opcje, method: 'GET' });
281 }
282
283 /**
284 * Wykonuje żądanie POST
285 * @param {string} endpoint - Endpoint API
286 * @param {Object} body - Ciało żądania
287 * @param {Object} opcje - Opcje żądania
288 * @returns {Promise} - Promise z wynikiem żądania
289 */
290 async post(endpoint, body, opcje = {}) {
291 return this.request(endpoint, { ...opcje, method: 'POST', body });
292 }
293
294 /**
295 * Wykonuje żądanie PUT
296 * @param {string} endpoint - Endpoint API
297 * @param {Object} body - Ciało żądania
298 * @param {Object} opcje - Opcje żądania
299 * @returns {Promise} - Promise z wynikiem żądania
300 */
301 async put(endpoint, body, opcje = {}) {
302 return this.request(endpoint, { ...opcje, method: 'PUT', body });
303 }
304
305 /**
306 * Wykonuje żądanie DELETE
307 * @param {string} endpoint - Endpoint API
308 * @param {Object} opcje - Opcje żądania
309 * @returns {Promise} - Promise z wynikiem żądania
310 */
311 async delete(endpoint, opcje = {}) {
312 return this.request(endpoint, { ...opcje, method: 'DELETE' });
313 }
314
315 /**
316 * Wysyła plik na serwer
317 * @param {string} endpoint - Endpoint API
318 * @param {File} plik - Plik do wysłania
319 * @param {Object} opcje - Opcje żądania
320 * @returns {Promise} - Promise z wynikiem żądania
321 */
322 async uploadFile(endpoint, plik, opcje = {}) {
323 const {
324 nazwaPolaPliku = 'file',
325 dodatkoweDane = {},
326 naPostep = null,
327 useAuth = true
328 } = opcje;
329
330 // Jeśli offline, dodaj do kolejki synchronizacji
331 if (!this.networkMonitor.pobierzStan().status === 'online') {
332 throw new Error('Nie można wysłać pliku w trybie offline.');
333 }
334
335 // Przygotowanie danych FormData
336 const formData = new FormData();
337 formData.append(nazwaPolaPliku, plik);
338
339 // Dodanie dodatkowych danych
340 Object.entries(dodatkoweDane).forEach(([klucz, wartosc]) => {
341 formData.append(klucz, JSON.stringify(wartosc));
342 });
343
344 // Opcje żądania
345 const requestOptions = {
346 method: 'POST',
347 body: formData
348 };
349
350 // Dodanie tokenu uwierzytelniającego, jeśli wymagane
351 if (useAuth) {
352 try {
353 const token = await this.auth.pobierzTokenDostepowy();
354 requestOptions.headers = {
355 'Authorization': `Bearer ${token}`
356 };
357 } catch (error) {
358 throw new Error('Brak uwierzytelnienia. Zaloguj się ponownie.');
359 }
360 }
361
362 return new Promise((resolve, reject) => {
363 const xhr = new XMLHttpRequest();
364
365 xhr.upload.addEventListener('progress', (event) => {
366 if (event.lengthComputable && typeof naPostep === 'function') {
367 const procent = Math.round((event.loaded / event.total) * 100);
368 naPostep({
369 loaded: event.loaded,
370 total: event.total,
371 procent
372 });
373 }
374 });
375
376 xhr.addEventListener('load', () => {
377 if (xhr.status >= 200 && xhr.status < 300) {
378 try {
379 const odpowiedz = JSON.parse(xhr.responseText);
380 resolve(odpowiedz);
381 } catch (e) {
382 resolve({
383 success: true,
384 status: xhr.status,
385 message: xhr.statusText
386 });
387 }
388 } else {
389 const blad = new Error(`Błąd HTTP: ${xhr.status}`);
390 blad.status = xhr.status;
391 blad.statusText = xhr.statusText;
392 reject(blad);
393 }
394 });
395
396 xhr.addEventListener('error', () => {
397 reject(new Error('Błąd sieci podczas wysyłania pliku'));
398 });
399
400 xhr.addEventListener('abort', () => {
401 reject(new Error('Wysyłanie pliku zostało przerwane'));
402 });
403
404 xhr.open('POST', `${this.baseUrl}${endpoint}`, true);
405
406 if (requestOptions.headers) {
407 Object.entries(requestOptions.headers).forEach(([key, value]) => {
408 xhr.setRequestHeader(key, value);
409 });
410 }
411
412 xhr.send(formData);
413 });
414 }
415
416 /**
417 * Uwierzytelnia użytkownika
418 * @param {string} email - Adres email
419 * @param {string} haslo - Hasło
420 * @returns {Promise} - Promise z wynikiem uwierzytelnienia
421 */
422 async login(email, haslo) {
423 try {
424 const response = await this.request('/auth/login', {
425 method: 'POST',
426 body: { email, password: haslo },
427 useAuth: false
428 });
429
430 // Zapisz tokeny
431 this.auth.zapiszTokeny(response);
432
433 return {
434 success: true,
435 user: response.user
436 };
437 } catch (error) {
438 return {
439 success: false,
440 error: error.message
441 };
442 }
443 }
444
445 /**
446 * Wylogowuje użytkownika
447 */
448 logout() {
449 this.auth.wyloguj();
450 // Opcjonalne wyczyszczenie cache przy wylogowaniu
451 this.cache.wyczysc();
452 }
453}
454
455// Utworzenie instancji klienta API
456const api = new JurassicAPIClient({
457 baseUrl: 'https://api.jurassic-park.com'
458});
459
460// Przykład użycia klienta API
461
462// Logowanie
463async function logowanie(email, haslo) {
464 const wynik = await api.login(email, haslo);
465 if (wynik.success) {
466 console.log(`Zalogowano jako ${wynik.user.name}`);
467 return true;
468 } else {
469 console.error(`Błąd logowania: ${wynik.error}`);
470 return false;
471 }
472}
473
474// Pobieranie danych dinozaura
475async function pobierzDinozaura(id) {
476 try {
477 return await api.get(`/dinosaurs/${id}`);
478 } catch (error) {
479 console.error(`Nie udało się pobrać dinozaura #${id}:`, error.message);
480 throw error;
481 }
482}
483
484// Aktualizacja danych dinozaura
485async function aktualizujDinozaura(id, dane) {
486 try {
487 return await api.put(`/dinosaurs/${id}`, dane);
488 } catch (error) {
489 console.error(`Nie udało się zaktualizować dinozaura #${id}:`, error.message);
490 throw error;
491 }
492}
493
494// Pobieranie listy dinozaurów z filtrowaniem
495async function pobierzListeDinozaurow(filter = {}) {
496 try {
497 return await api.get('/dinosaurs', {
498 body: filter,
499 cacheTime: 60000 // Krótszy czas cache dla list (1 minuta)
500 });
501 } catch (error) {
502 console.error('Nie udało się pobrać listy dinozaurów:', error.message);
503 throw error;
504 }
505}
506
507// Wysyłanie dokumentacji medycznej dinozaura
508async function wyslijDokumentacjeMedyczna(dinoId, plik, dane) {
509 const progressBar = document.getElementById('upload-progress');
510 const statusText = document.getElementById('upload-status');
511
512 try {
513 return await api.uploadFile(`/dinosaurs/${dinoId}/medical-records`, plik, {
514 dodatkoweDane: dane,
515 naPostep: (info) => {
516 if (progressBar) progressBar.value = info.procent;
517 if (statusText) statusText.textContent = `Wysyłanie: ${info.procent}%`;
518 }
519 });
520 } catch (error) {
521 console.error(`Nie udało się wysłać dokumentacji dla dinozaura #${dinoId}:`, error.message);
522 if (statusText) statusText.textContent = `Błąd: ${error.message}`;
523 throw error;
524 }
525}W tym obszernym przykładzie zaimplementowaliśmy zaawansowany klient API, który obsługuje:
Taki kompleksowy klient API zapewnia odporność na problemy sieciowe, efektywność dzięki cachowaniu, wsparcie dla trybu offline oraz przyjazne dla użytkownika mechanizmy, takie jak monitorowanie postępu. Jest to idealne rozwiązanie dla krytycznych systemów, takich jak system zarządzania Parkiem Jurajskim, gdzie niezawodność i bezpieczeństwo są kluczowe.
W następnej lekcji przyjrzymy się, jak efektywnie przetwarzać dane JSON otrzymane z API, aby prezentować je użytkownikom w przystępny sposób.