Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Obsługa żądań HTTP - Praktyczne wzorce i zaawansowane techniki

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.

Kompleksowa obsługa błędów

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}

Wzorzec retry - automatyczne ponawianie żądań

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}

Kolejkowanie żądań i limit równoległych połączeń

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}

Zarządzanie danymi w pamięci podręcznej (caching)

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}

Obsługa żądań synchronizowanych z serwerem

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}

Uwierzytelnianie i odświeżanie tokenów

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}

Przesyłanie plików z postępem

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}

Monitorowanie stanu sieci

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}

Podsumowanie - Tworzenie kompleksowego klienta API

Łą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:

  1. Kompleksową obsługę błędów - różne rodzaje błędów HTTP i sieciowych
  2. Automatyczne ponawianie żądań - w przypadku tymczasowych problemów
  3. Cachowanie odpowiedzi - dla szybszego dostępu i wsparcia trybu offline
  4. Kolejkowanie operacji w trybie offline - dla zapewnienia spójności danych
  5. Monitorowanie stanu sieci - dla podejmowania odpowiednich decyzji
  6. Uwierzytelnianie i odświeżanie tokenów - dla bezpiecznego dostępu do API
  7. Wysyłanie plików z monitorowaniem postępu - dla lepszego doświadczenia użytkownika

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.

Vai a CodeWorlds