We use cookies to enhance your experience on the site
CodeWorlds

HTTP Request Handling — Practical Patterns and Advanced Techniques

In the previous exercise we covered the basics of the Fetch API. Now let's expand our knowledge with more advanced techniques and practical patterns to build a robust communication system for our Jurassic Park.

Comprehensive Error Handling

In an environment as critical as Jurassic Park, where a system failure could lead to dangerous situations, proper error handling in HTTP communication is extremely important. Let's create a more advanced error handling mechanism:

1async function bezpieczneZapytanie(url, options = {}) {
2  const controller = new AbortController();
3  const { signal } = controller;
4
5  // Set timeout (10 seconds)
6  const timeoutId = setTimeout(() => controller.abort(), 10000);
7
8  try {
9    const response = await fetch(url, { ...options, signal });
10    clearTimeout(timeoutId);
11
12    // HTTP error categories
13    if (response.status >= 500) {
14      throw new Error('Server error, attemptNumbly a temporary issue.');
15    } else if (response.status === 404) {
16      throw new Error('Resource not found.');
17    } else if (response.status === 403) {
18      throw new Error('No permission to access this resource.');
19    } else if (response.status === 401) {
20      throw new Error('Authentication required. Please log in again.');
21    } else if (!response.ok) {
22      throw new Error(`Unexpected HTTP error: ${response.status}`);
23    }
24
25    // Handle different response types
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    // Handle different error types
38    if (error.name === 'AbortError') {
39      throw new Error('Request aborted - time limit exceeded (10s).');
40    } else if (!navigator.onLine) {
41      throw new Error('No internet connection. Check your network connection.');
42    } else {
43      console.error('Error details:', error);
44      throw new Error(`Error communicating with server: ${error.message}`);
45    }
46  }
47}

Retry Pattern — Automatic Request Retries

In the Jurassic Park network, temporary connectivity issues can occur, especially in remote parts of the island. Let's implement an automatic retry mechanism for failed requests:

1/**
2 * Executes an HTTP request with automatic retries
3 * @param {Function} requestFn - Function performing the request, returning a Promise
4 * @param {Object} options - Configuration options
5 * @returns {Promise} - Request result or last error after exhausting attempts
6 */
7async function withRetry(requestFn, options = {}) {
8  const {
9    maxAttempts = 3,
10    initialDelay = 1000,
11    delayFactor = 2,
12    maxDelay = 10000,
13    timeoutMs = 10000,
14    shouldRetry = (error) => {
15      // By default retry only for server errors (5xx) and network issues
16      return (
17        error.message.includes('Server error') ||
18        error.message.includes('No internet') ||
19        error.name === 'AbortError'
20      );
21    }
22  } = options;
23
24  let lastError;
25  let attemptNum = 0;
26
27  while (attemptNum <= maxAttempts) {
28    try {
29      // For each attempt, create a new abort controller
30      const controller = new AbortController();
31      const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
32
33      // If it's not the first attempt, wait before retrying
34      if (attemptNum > 0) {
35        const delayTime = Math.min(
36          initialDelay * Math.pow(delayFactor, attemptNum - 1),
37          maxDelay
38        );
39        console.log(`Attempt ${attemptNum}/${maxAttempts} failed. Retrying in ${delayTime}ms...`);
40        await new Promise(resolve => setTimeout(resolve, delayTime));
41      }
42
43      // Execute the request
44      const result = await requestFn(controller.signal);
45      clearTimeout(timeoutId);
46      return result;
47    } catch (error) {
48      clearTimeout(timeoutId);
49      lastError = error;
50
51      // Check whether to retry
52      if (attemptNum === maxAttempts || !shouldRetry(error)) {
53        break;
54      }
55
56      attemptNum++;
57    }
58  }
59
60  // Throw the last error after exhausting all attempts
61  throw new Error(`Failed after ${maxAttempts} attemptch: ${lastError.message}`);
62}
63
64// Usage example
65async function fetchDinosaurInfo(id) {
66  return withRetry(
67    async (signal) => {
68      const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, { signal });
69
70      if (!response.ok) {
71        throw new Error(`HTTP Error: ${response.status}`);
72      }
73
74      return response.json();
75    },
76    {
77      maxAttempts: 5,
78      initialDelay: 500,
79      shouldRetry: (error) => error.message.includes('HTTP Error: 5')
80    }
81  );
82}

Request Queuing and Concurrent Connection Limits

In situations where we need to make many HTTP requests but want to limit the number of simultaneous connections so we don't overload the network or server, we can implement request queuing:

1class KolejkaZadan {
2  constructor(maxRownoleglosc = 3) {
3    this.maxRownoleglosc = maxRownoleglosc;
4    this.aktywneZadania = 0;
5    this.kolejka = [];
6  }
7
8  async dodaj(zadanie) {
9    // If there's room for more parallel tasks, execute immediately
10    if (this.aktywneZadania < this.maxRownoleglosc) {
11      return this.wykonaj(zadanie);
12    }
13
14    // Otherwise add to the queue
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      // Execute the task
25      const result = await zadanie();
26      return result;
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    // Get the next task from the queue
41    const { zadanie, resolve, reject } = this.kolejka.shift();
42
43    // Execute the task and forward the result to the waiting Promise
44    this.wykonaj(zadanie)
45      .then(resolve)
46      .catch(reject);
47  }
48}
49
50// Example usage of the queue to monitor many dinosaurs
51async function monitorWszystkieDinosaury(idList) {
52  const kolejka = new KolejkaZadan(5); // Max 5 concurrent requests
53  const results = [];
54  const bledy = [];
55
56  // Helper functions for tracking progress
57  const totalZadan = idList.length;
58  let ukonczoneZadania = 0;
59
60  function aktualizujPostep() {
61    ukonczoneZadania++;
62    const procent = Math.floor((ukonczoneZadania / totalZadan) * 100);
63    console.log(`Monitoring progress: ${procent}% (${ukonczoneZadania}/${totalZadan})`);
64  }
65
66  // Create an array of Promises for all tasks
67  const promisy = idList.map(id => {
68    return kolejka.dodaj(async () => {
69      try {
70        console.log(`Monitorowanie dinosaura #${id}...`);
71        const result = await fetchDinosaurInfo(id);
72        results.push(result);
73        aktualizujPostep();
74        return result;
75      } catch (error) {
76        bledy.push({ id, error: error.message });
77        aktualizujPostep();
78        throw error;
79      }
80    }).catch(error => {
81      // Catch errors so we don't interrupt the entire process
82      console.error(`Error monitoring dinosaur #${id}:`, error.message);
83    });
84  });
85
86  // Wait for all tasks to finish
87  await Promise.all(promisy);
88
89  return {
90    results,
91    bledy,
92    podsumowanie: {
93      total: totalZadan,
94      sukces: results.length,
95      blad: bledy.length
96    }
97  };
98}

Cache Data Management (Caching)

In Jurassic Park we have data that changes rarely (e.g., dinosaur species) and data that changes frequently (e.g., dinosaur locations). Implementing a cache will help optimize communication:

1class ZarzadcaPamieciPodrecznej {
2  constructor() {
3    this.cache = new Map();
4    this.statystyki = {
5      trafienia: 0,
6      pudla: 0,
7      invalidations: 0
8    };
9  }
10
11  /**
12   * Retrieves data from cache or executes the fetch function if data is not cached
13   * @param {string} klucz - Unique data identifier in cache
14   * @param {Function} pobierzFn - Function to fetch data if not in cache
15   * @param {Object} options - Cache options
16   * @returns {Promise} - Promise with data
17   */
18  async pobierz(klucz, pobierzFn, options = {}) {
19    const {
20      ttl = 60000, // Default TTL: 1 minute
21      forceRefresh = false, // Whether to force refresh despite being cached
22      onlyIfCached = false // Whether to return only from cache (no network request)
23    } = options;
24
25    // Check if data is in cache and still valid
26    const wpis = this.cache.get(klucz);
27    const teraz = Date.now();
28
29    if (wpis && !forceRefresh && teraz < wpis.expiresAt) {
30      // Data found in cache and still valid
31      this.statystyki.trafienia++;
32      return wpis.data;
33    }
34
35    // If we only want cached data and there's none, return null
36    if (onlyIfCached) {
37      return null;
38    }
39
40    // Data not in cache or we're forcing a refresh
41    this.statystyki.pudla++;
42
43    try {
44      // Fetch data
45      const data = await pobierzFn();
46
47      // Save in cache
48      this.cache.set(klucz, {
49        data,
50        expiresAt: teraz + ttl,
51        dodanoAt: teraz
52      });
53
54      return data;
55    } catch (error) {
56      // If we have stale data in cache, we can return it as a fallback
57      if (wpis) {
58        console.warn(`Error fetching data for ${klucz}, using stale data from cache:`, error);
59        return wpis.data;
60      }
61
62      // Otherwise propagate the error
63      throw error;
64    }
65  }
66
67  /**
68   * Invalidates cache entries matching a pattern
69   * @param {string|RegExp} wzorzec - Pattern to match cache keys
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.invalidations += licznik;
83    return licznik;
84  }
85
86  /**
87   * Clears all cache entries
88   */
89  wyczysc() {
90    const licznik = this.cache.size;
91    this.cache.clear();
92    this.statystyki.invalidations += licznik;
93    return licznik;
94  }
95
96  /**
97   * Returns cache usage statistics
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// Create a cache manager instance
113const cacheDinosaurow = new ZarzadcaPamieciPodrecznej();
114
115// Usage example
116async function fetchDinosaurInfoZCache(id, options = {}) {
117  return cacheDinosaurow.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(`HTTP Error: ${response.status}`);
123      }
124      return response.json();
125    },
126    {
127      ttl: 300000, // 5 minutes for dinosaur data
128      ...options
129    }
130  );
131}
132
133// Function to invalidate cache after an update
134async function aktualizujDinosaura(id, data) {
135  const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
136    method: 'PUT',
137    headers: { 'Content-Type': 'application/json' },
138    body: JSON.stringify(data)
139  });
140
141  if (!response.ok) {
142    throw new Error(`Update error: ${response.status}`);
143  }
144
145  // Invalidate this dinosaur's cache
146  cacheDinosaurow.uniewaznij(`dino-${id}`);
147
148  // Also invalidate dinosaur lists that might contain this dinosaur
149  cacheDinosaurow.uniewaznij('dino-list');
150
151  return response.json();
152}
153
154// Fetch dinosaur list with shorter cache (data changes more frequently)
155async function pobierzListeDinosaurow(filter = {}) {
156  const queryParams = new URLSearchParams(filter).toString();
157  const cacheKey = `dino-list-${queryParams || 'all'}`;
158
159  return cacheDinosaurow.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(`HTTP Error: ${response.status}`);
166      }
167      return response.json();
168    },
169    { ttl: 60000 } // 1 minute for dinosaur lists
170  );
171}

Server-Synchronized Request Handling

In Jurassic Park we sometimes need to perform operations that require server synchronization, such as updating dinosaur health statuses. Let's implement a mechanism that ensures data is saved even when connectivity issues occur:

1class KolejkaSynchronizacji {
2  constructor(options = {}) {
3    // localStorage key for storing the queue
4    this.klucz = options.klucz || 'dinosaurs-sync-queue';
5
6    // Load saved queue
7    this.kolejka = this.zaladujKolejke();
8
9    // Flag indicating whether synchronization is active
10    this.jestSynchronizacja = false;
11
12    // Auto-start synchronization
13    if (options.autoSync !== false) {
14      this.rozpocznijSynchronizacje();
15    }
16
17    // Listen for online/offline state changes
18    window.addEventListener('online', () => {
19      console.log('Connection restored. Starting synchronization...');
20      this.rozpocznijSynchronizacje();
21    });
22
23    window.addEventListener('offline', () => {
24      console.log('Connection lost. Synchronization paused.');
25    });
26  }
27
28  /**
29   * Loads the queue from 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('Error loading sync queue:', error);
37      return [];
38    }
39  }
40
41  /**
42   * Saves the queue to localStorage
43   */
44  zapiszKolejke() {
45    try {
46      localStorage.setItem(this.klucz, JSON.stringify(this.kolejka));
47    } catch (error) {
48      console.error('Error saving sync queue:', error);
49    }
50  }
51
52  /**
53   * Adds an operation to the sync queue
54   * @param {string} url - Request URL
55   * @param {Object} options - Fetch options
56   * @param {string} typ - Operation type (e.g. 'aktualizacja-zdrowia')
57   * @returns {string} - Operation ID in the queue
58   */
59  dodajDoKolejki(url, options = {}, 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      options,
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   * Starts the synchronization process
79   */
80  async rozpocznijSynchronizacje() {
81    // If already syncing or no connection, do nothing
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 operation = this.kolejka[index];
92
93        try {
94          console.log(`Synchronizing operation: ${operation.typ} (${operation.id})`);
95
96          // Increment attempt count
97          operation.proby++;
98          this.zapiszKolejke();
99
100          // Execute the request
101          const response = await fetch(operation.url, operation.options);
102
103          if (!response.ok) {
104            throw new Error(`HTTP Error: ${response.status}`);
105          }
106
107          // Operation succeeded, remove from queue
108          this.kolejka.splice(index, 1);
109          this.zapiszKolejke();
110
111          console.log(`Operation ${operation.id} synchronized successfully`);
112
113          // Don't increment index since we removed the element
114        } catch (error) {
115          console.error(`Error synchronizing operation ${operation.id}:`, error);
116
117          // If we've reached max attempts, mark as failed
118          if (operation.proby >= 5) {
119            console.warn(`Operation ${operation.id} exceeded retry limit. Marking as failed.`);
120            operation.status = 'nieudana';
121            operation.bladKomunikat = error.message;
122            this.zapiszKolejke();
123          }
124
125          // Move to next operation
126          index++;
127        }
128      }
129    } finally {
130      this.jestSynchronizacja = false;
131
132      // If there are still items in the queue, retry in 1 minute
133      if (this.kolejka.some(op => !op.status || op.status !== 'nieudana')) {
134        setTimeout(() => this.rozpocznijSynchronizacje(), 60000);
135      }
136    }
137  }
138
139  /**
140   * Removes failed operations from the queue
141   */
142  wyczyscNieudataOperacje() {
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   * Returns the current queue status
151   */
152  pobierzStatus() {
153    const wszystkie = this.kolejka.length;
154    const nieudata = this.kolejka.filter(op => op.status === 'nieudana').length;
155    const oczekujace = wszystkie - nieudata;
156
157    return {
158      wszystkie,
159      oczekujace,
160      nieudata,
161      jestSynchronizacja: this.jestSynchronizacja
162    };
163  }
164}
165
166// Create sync queue instance
167const syncDinosaurow = new KolejkaSynchronizacji({
168  klucz: 'jurassic-park-dino-sync'
169});
170
171// Usage example: updating dinosaur health that will be synchronized
172function aktualizujZdrowieDinosaura(id, parametryZdrowia) {
173  // Prepare data for sending
174  const data = {
175    id,
176    health: parametryZdrowia.health,
177    lastCheckTime: new Date().toISOString(),
178    symptoms: parametryZdrowia.symptoms || [],
179    updatedBy: 'dr-wu'
180  };
181
182  // If we're online, try immediately
183  if (navigator.onLine) {
184    return fetch(`https://api.jurassic-park.com/dinosaurs/${id}/health`, {
185      method: 'PUT',
186      headers: { 'Content-Type': 'application/json' },
187      body: JSON.stringify(data)
188    })
189    .then(response => {
190      if (!response.ok) {
191        throw new Error(`HTTP Error: ${response.status}`);
192      }
193      return response.json();
194    })
195    .catch(error => {
196      console.warn('Failed to update health online, adding to sync queue');
197
198      // Add to sync queue
199      syncDinosaurow.dodajDoKolejki(
200        `https://api.jurassic-park.com/dinosaurs/${id}/health`,
201        {
202          method: 'PUT',
203          headers: { 'Content-Type': 'application/json' },
204          body: JSON.stringify(data)
205        },
206        `aktualizacja-zdrowia-${id}`
207      );
208
209      // Return data as if the operation succeeded, though it will be synced later
210      return {
211        ...data,
212        pendingSync: true
213      };
214    });
215  } else {
216    // If we're offline, add to queue immediately
217    syncDinosaurow.dodajDoKolejki(
218      `https://api.jurassic-park.com/dinosaurs/${id}/health`,
219      {
220        method: 'PUT',
221        headers: { 'Content-Type': 'application/json' },
222        body: JSON.stringify(data)
223      },
224      `aktualizacja-zdrowia-${id}`
225    );
226
227    return Promise.resolve({
228      ...data,
229      pendingSync: true
230    });
231  }
232}

Authentication and Token Refresh

In the Jurassic Park system we need to secure access to the API using authentication tokens. Let's implement a mechanism that automatically refreshes the token when it expires:

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   * Gets the current access token
12   * @param {boolean} autoRefresh - Whether to auto-refresh the token if expired
13   * @returns {Promise<string>} - Promise with the access token
14   */
15  async pobierzTokenDostepowy(autoRefresh = true) {
16    const token = localStorage.getItem(this.tokenAccessKey);
17    const expiry = localStorage.getItem(this.expiryKey);
18
19    // Check if token exists and hasn't expired
20    if (token && expiry) {
21      const expiryDate = new Date(expiry);
22      const now = new Date();
23
24      // Add 1-minute buffer to avoid using a token that's about to expire
25      const bufferTime = 60 * 1000; // 1 minute in milliseconds
26
27      if (expiryDate.getTime() - now.getTime() > bufferTime) {
28        return token;
29      }
30
31      // Token has expired or will expire soon
32      if (autoRefresh) {
33        return this.odswiezToken();
34      }
35    }
36
37    throw new Error('No valid access token');
38  }
39
40  /**
41   * Refreshes the access token using the refresh token
42   * @returns {Promise<string>} - Promise with the new access token
43   */
44  async odswiezToken() {
45    // If a refresh is already in progress, wait for its result
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('No refresh token');
59      }
60
61      const response = await fetch('https://api.jurassic-park.com/auth/refresh', {
62        method: 'POST',
63        headers: { 'Content-Type': 'application/json' },
64        body: JSON.stringify({ refresh_token: refreshToken })
65      });
66
67      if (!response.ok) {
68        if (response.status === 401) {
69          // Refresh token is invalid, log the user out
70          this.wyloguj();
71          throw new Error('Session expired. Please log in again.');
72        }
73        throw new Error(`Token refresh error: ${response.status}`);
74      }
75
76      const data = await response.json();
77
78      // Save new tokens
79      localStorage.setItem(this.tokenAccessKey, data.access_token);
80      localStorage.setItem(this.tokenRefreshKey, data.refresh_token);
81
82      // Calculate expiry time
83      const expiry = new Date();
84      expiry.setSeconds(expiry.getSeconds() + data.expires_in);
85      localStorage.setItem(this.expiryKey, expiry.toISOString());
86
87      // Notify all waiting listeners
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      // On error, notify all waiting listeners
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   * Saves tokens after login
109   * @param {Object} authData - Authentication data
110   */
111  zapiszTokeny(authData) {
112    localStorage.setItem(this.tokenAccessKey, authData.access_token);
113    localStorage.setItem(this.tokenRefreshKey, authData.refresh_token);
114
115    // Calculate expiry time
116    const expiry = new Date();
117    expiry.setSeconds(expiry.getSeconds() + authData.expires_in);
118    localStorage.setItem(this.expiryKey, expiry.toISOString());
119  }
120
121  /**
122   * Logs the user out
123   */
124  wyloguj() {
125    localStorage.removeItem(this.tokenAccessKey);
126    localStorage.removeItem(this.tokenRefreshKey);
127    localStorage.removeItem(this.expiryKey);
128  }
129
130  /**
131   * Checks if the user is logged in
132   * @returns {boolean}
133   */
134  czyZalogowany() {
135    return !!localStorage.getItem(this.tokenAccessKey);
136  }
137}
138
139// Create auth manager instance
140const auth = new ZarzadcaUwierzytelniania();
141
142// Usage example - function for making authenticated requests
143async function queryUwierzytelnione(url, options = {}) {
144  try {
145    // Get access token (with auto-refresh)
146    const token = await auth.pobierzTokenDostepowy();
147
148    // Add token to headers
149    const authOptions = {
150      ...options,
151      headers: {
152        ...options.headers,
153        'Authorization': `Bearer ${token}`
154      }
155    };
156
157    // Execute request
158    const response = await fetch(url, authOptions);
159
160    // Handle authorization errors
161    if (response.status === 401) {
162      // Try refreshing the token and retrying
163      try {
164        const newToken = await auth.odswiezToken();
165
166        // Retry request with new token
167        const refreshedOptions = {
168          ...options,
169          headers: {
170            ...options.headers,
171            'Authorization': `Bearer ${newToken}`
172          }
173        };
174
175        return fetch(url, refreshedOptions);
176      } catch (refreshError) {
177        // Token refresh failed, log user out
178        auth.wyloguj();
179        window.location.href = '/login'; // Redirect to login
180        throw new Error('Session expired. Please log in again.');
181      }
182    }
183
184    return response;
185  } catch (error) {
186    console.error('Authenticated request error:', error);
187    throw error;
188  }
189}
190
191// Usage example
192async function fetchEnclosureData(enclosureId) {
193  const response = await queryUwierzytelnione(
194    `https://api.jurassic-park.com/paddocks/${enclosureId}`
195  );
196
197  if (!response.ok) {
198    throw new Error(`Error fetching enclosure data: ${response.status}`);
199  }
200
201  return response.json();
202}
203
204// Login function
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(`Login error: ${response.status}`);
215    }
216
217    const authData = await response.json();
218
219    // Save tokens
220    auth.zapiszTokeny(authData);
221
222    return {
223      success: true,
224      user: authData.user
225    };
226  } catch (error) {
227    console.error('Login error:', error);
228    return {
229      success: false,
230      error: error.message
231    };
232  }
233}

File Upload with Progress

In Jurassic Park we often need to upload large files, such as images or videos from dinosaur monitoring cameras. Let's implement a function that handles file uploads with progress reporting:

1/**
2 * Sends a file to the server with progress monitoring
3 * @param {File} plik - File to send
4 * @param {string} url - Target URL
5 * @param {Object} options - Additional options
6 * @returns {Promise} - Promise with operation result
7 */
8async function wyslijPlik(plik, url, options = {}) {
9  return new Promise((resolve, reject) => {
10    // Default options
11    const {
12      metoda = 'POST',
13      nazwaPolaPliku = 'file',
14      dodatkoweDane = {},
15      naPostep = null,
16      naBlad = null,
17      maxRetries = 3
18    } = options;
19
20    // Retry counter
21    let proby = 0;
22
23    // Function to execute the upload
24    function wykonajWysylanie() {
25      // Create XHR object
26      const xhr = new XMLHttpRequest();
27
28      // Configure event handlers
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            // Parse JSON response
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(`HTTP Error: ${xhr.status}`);
55          blad.status = xhr.status;
56          blad.statusText = xhr.statusText;
57
58          if (xhr.status >= 500 && proby < maxRetries) {
59            // For server errors, retry
60            proby++;
61            console.warn(`File upload error (${xhr.status}). Retry attempt ${proby}/${maxRetries}...`);
62            setTimeout(wykonajWysylanie, 2000 * proby); // Increasing delay between attempts
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('Network error during file upload');
74        if (typeof naBlad === 'function') {
75          naBlad(blad);
76        }
77
78        if (proby < maxRetries) {
79          proby++;
80          console.warn(`Network error during upload. Retry attempt ${proby}/${maxRetries}...`);
81          setTimeout(wykonajWysylanie, 2000 * proby);
82        } else {
83          reject(blad);
84        }
85      });
86
87      xhr.addEventListener('abort', () => {
88        const blad = new Error('File upload was aborted');
89        if (typeof naBlad === 'function') {
90          naBlad(blad);
91        }
92        reject(blad);
93      });
94
95      // Open connection
96      xhr.open(metoda, url, true);
97
98      // Prepare FormData
99      const formData = new FormData();
100      formData.append(nazwaPolaPliku, plik);
101
102      // Add additional data
103      Object.entries(dodatkoweDane).forEach(([klucz, wartosc]) => {
104        formData.append(klucz, wartosc);
105      });
106
107      // Send request
108      xhr.send(formData);
109
110      // Return object with abort method
111      return {
112        przerwij: () => xhr.abort()
113      };
114    }
115
116    // Start uploading
117    wykonajWysylanie();
118  });
119}
120
121// Usage example
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 = 'Preparing...';
129
130  try {
131    const result = 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 = `Uploading: ${info.procent}%`;
143        },
144        naBlad: (blad) => {
145          statusText.textContent = `Error: ${blad.message}`;
146          progressBar.classList.add('error');
147        }
148      }
149    );
150
151    statusText.textContent = 'Upload complete!';
152    progressBar.value = 100;
153
154    setTimeout(() => {
155      controller.classList.add('hidden');
156    }, 3000);
157
158    return result;
159  } catch (error) {
160    statusText.textContent = `Error: ${error.message}`;
161    progressBar.classList.add('error');
162    throw error;
163  }
164}

Network State Monitoring

In Jurassic Park we must be prepared for connectivity issues, especially in remote parts of the island. Let's implement a network state monitoring system:

1class MonitorSieci {
2  constructor(options = {}) {
3    this.sluchacze = {
4      zmiana: [],
5      online: [],
6      offline: []
7    };
8
9    this.ostatniPing = null;
10    this.delayTime = null;
11    this.status = navigator.onLine ? 'online' : 'offline';
12    this.adresEndpointu = options.endpointPing || 'https://api.jurassic-park.com/ping';
13    this.interwalPingu = options.interwalPingu || 30000; // 30 seconds
14    this.timeoutPingu = options.timeoutPingu || 5000; // 5 seconds
15
16    // Add listeners for online/offline events
17    window.addEventListener('online', this.obslugaOnline.bind(this));
18    window.addEventListener('offline', this.obslugaOffline.bind(this));
19
20    // Start regular pinging
21    if (options.autostartPing !== false) {
22      this.startPingowanie();
23    }
24  }
25
26  /**
27   * Handles transition to online mode
28   */
29  obslugaOnline() {
30    const poprzedniStatus = this.status;
31    this.status = 'online';
32
33    // Notify listeners
34    this.powiadomSluchaczy('online');
35
36    if (poprzedniStatus !== this.status) {
37      this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
38    }
39
40    // Perform immediate ping to check real connectivity
41    this.ping();
42  }
43
44  /**
45   * Handles transition to offline mode
46   */
47  obslugaOffline() {
48    const poprzedniStatus = this.status;
49    this.status = 'offline';
50    this.delayTime = null;
51
52    // Notify listeners
53    this.powiadomSluchaczy('offline');
54
55    if (poprzedniStatus !== this.status) {
56      this.powiadomSluchaczy('zmiana', { status: this.status, poprzedni: poprzedniStatus });
57    }
58  }
59
60  /**
61   * Pings the server to check real connectivity
62   * @returns {Promise<number>} - Promise with latency in ms
63   */
64  async ping() {
65    if (!navigator.onLine) {
66      this.delayTime = null;
67      return Promise.reject(new Error('Browser is in offline mode'));
68    }
69
70    try {
71      const startTime = Date.now();
72
73      // Set timeout for 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(`HTTP Error: ${response.status}`);
87      }
88
89      // Calculate latency
90      const endTime = Date.now();
91      this.delayTime = endTime - startTime;
92      this.ostatniPing = new Date();
93
94      // Update status
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.delayTime;
104    } catch (error) {
105      // If timeout or network error, mark as offline
106      if (error.name === 'AbortError' || error instanceof TypeError) {
107        const poprzedniStatus = this.status;
108        this.status = 'offline';
109        this.delayTime = 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   * Starts regular server pinging
123   */
124  startPingowanie() {
125    // Stop existing interval if any
126    this.stopPingowanie();
127
128    // Perform first ping immediately
129    this.ping().catch(e => console.warn('Initial ping error:', e));
130
131    // Set ping interval
132    this.pingInterval = setInterval(() => {
133      this.ping().catch(e => console.warn('Ping error:', e));
134    }, this.interwalPingu);
135  }
136
137  /**
138   * Stops regular server pinging
139   */
140  stopPingowanie() {
141    if (this.pingInterval) {
142      clearInterval(this.pingInterval);
143      this.pingInterval = null;
144    }
145  }
146
147  /**
148   * Adds an event listener
149   * @param {string} typZdarzenia - Event type ('zmiana', 'online', 'offline')
150   * @param {Function} callback - Callback function
151   */
152  nasluchuj(typZdarzenia, callback) {
153    if (this.sluchacze[typZdarzenia]) {
154      this.sluchacze[typZdarzenia].push(callback);
155    }
156    return this; // For chain API
157  }
158
159  /**
160   * Removes a listener
161   * @param {string} typZdarzenia - Event type
162   * @param {Function} callback - Callback function to remove
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   * Notifies listeners about an event
173   * @param {string} typZdarzenia - Event type
174   * @param {Object} data - Event data
175   */
176  powiadomSluchaczy(typZdarzenia, data = {}) {
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            delayTime: this.delayTime,
185            ...data
186          });
187        } catch (e) {
188          console.error(`Error in listener ${typZdarzenia}:`, e);
189        }
190      });
191    }
192  }
193
194  /**
195   * Returns current network state
196   */
197  pobierzStan() {
198    return {
199      status: this.status,
200      delayTime: this.delayTime,
201      ostatniPing: this.ostatniPing,
202      przegldarkaOnline: navigator.onLine
203    };
204  }
205}
206
207// Create network monitor instance
208const monitorSieci = new MonitorSieci();
209
210// Usage example
211monitorSieci.nasluchuj('zmiana', (info) => {
212  console.log(`Zmiana stanu sieci: ${info.poprzedni} -> ${info.status}`);
213
214  // Update UI
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.delayTime}ms)`
220      : 'Offline';
221  }
222});
223
224monitorSieci.nasluchuj('offline', () => {
225  // Show notification to user
226  pokazPowiadomienie('Lost network connection', 'error');
227
228  // Enable offline mode in the app
229  app.wlaczTrybOffline();
230});
231
232monitorSieci.nasluchuj('online', (info) => {
233  pokazPowiadomienie(`Connection restored (ping: ${info.delayTime}ms)`, 'success');
234
235  // Sync data with server
236  syncDinosaurow.rozpocznijSynchronizacje();
237
238  // Disable offline mode
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  // Remove notification after 5 seconds
250  setTimeout(() => {
251    notification.classList.add('fade-out');
252    setTimeout(() => {
253      document.body.removeChild(notification);
254    }, 500);
255  }, 5000);
256}

Summary — Building a Comprehensive API Client

By combining all the techniques discussed, we can create a highly efficient, error-resilient, and user-friendly API communication system. Here's a complete solution:

1/**
2 * Comprehensive API client for the Jurassic Park system
3 */
4class JurassicAPIClient {
5  constructor(options = {}) {
6    // Base URL configuration
7    this.baseUrl = options.baseUrl || 'https://api.jurassic-park.com';
8
9    // Initialize auth manager
10    this.auth = new ZarzadcaUwierzytelniania();
11
12    // Initialize cache
13    this.cache = new ZarzadcaPamieciPodrecznej();
14
15    // Initialize sync queue
16    this.syncQueue = new KolejkaSynchronizacji({
17      klucz: 'jp-sync-queue',
18      autoSync: true
19    });
20
21    // Initialize network monitor
22    this.networkMonitor = new MonitorSieci({
23      endpointPing: `${this.baseUrl}/ping`,
24      interwalPingu: 30000
25    });
26
27    // Monitor network state
28    this.networkMonitor.nasluchuj('online', () => {
29      this.syncQueue.rozpocznijSynchronizacje();
30    });
31
32    // Default request options
33    this.defaultOptions = {
34      timeout: 30000,
35      retries: 3,
36      cacheTime: 300000, // 5 minutes
37      useCache: true
38    };
39  }
40
41  /**
42   * Executes an HTTP request
43   * @param {string} endpoint - API endpoint
44   * @param {Object} options - Request options
45   * @returns {Promise} - Promise with request result
46   */
47  async request(endpoint, options = {}) {
48    // Merge default options with provided ones
49    const {
50      method = 'GET',
51      body,
52      headers = {},
53      useAuth = true,
54      useCache = this.defaultOptions.useCache,
55      cacheTime = this.defaultOptions.cacheTime,
56      forceRefresh = false,
57      timeout = this.defaultOptions.timeout,
58      retries = this.defaultOptions.retries,
59      offlineFallback = true
60    } = options;
61
62    // Prepare cache key
63    const cacheKey = `${method}:${endpoint}:${JSON.stringify(body || {})}`;
64
65    // Check if request is in cache (only for GET)
66    if (method === 'GET' && useCache && !forceRefresh) {
67      try {
68        const cachedData = await this.cache.pobierz(
69          cacheKey,
70          () => {}, // Empty function — we don't want to execute the request yet
71          { ttl: cacheTime, onlyIfCached: true }
72        );
73
74        if (cachedData) {
75          return cachedData;
76        }
77      } catch (error) {
78        // Ignore cache errors
79      }
80    }
81
82    // Check network state
83    if (!this.networkMonitor.pobierzStan().status === 'online') {
84      // We're offline, check if we have cached data
85      if (method === 'GET' && useCache && offlineFallback) {
86        try {
87          const cachedData = await this.cache.pobierz(
88            cacheKey,
89            () => {}, // Empty function — we don't want to execute the request yet
90            { ttl: Infinity, onlyIfCached: true, ignoreExpiry: true }
91          );
92
93          if (cachedData) {
94            // Offline mode information
95            return {
96              ...cachedData,
97              _meta: {
98                fromCache: true,
99                offlineMode: true,
100                timestamp: new Date()
101              }
102            };
103          }
104        } catch (error) {
105          // Ignore cache errors
106        }
107      }
108
109      // If it's a data-modifying request, add to sync queue
110      if (method !== 'GET' && offlineFallback) {
111        return this.addToSyncQueue(endpoint, { method, body, headers, useAuth });
112      }
113
114      throw new Error('No network connection. Unable to execute request.');
115    }
116
117    // Function that executes the actual request
118    const executeRequest = async (signal) => {
119      // Prepare URL
120      const url = `${this.baseUrl}${endpoint}`;
121
122      // Prepare request options
123      const requestOptions = {
124        method,
125        headers: {
126          'Content-Type': 'application/json',
127          ...headers
128        },
129        signal
130      };
131
132      // Add request body if it exists
133      if (body) {
134        requestOptions.body = JSON.stringify(body);
135      }
136
137      // Add auth token if required
138      if (useAuth) {
139        try {
140          const token = await this.auth.pobierzTokenDostepowy();
141          requestOptions.headers['Authorization'] = `Bearer ${token}`;
142        } catch (error) {
143          // If unable to get token and request requires auth
144          throw new Error('Not authenticated. Please log in again.');
145        }
146      }
147
148      // Execute request with timeout
149      const controller = new AbortController();
150      const { signal: abortSignal } = controller;
151      if (signal) {
152        // If an external signal was passed, combine it with ours
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        // Handle HTTP errors
164        if (response.status === 401 && useAuth) {
165          // Try refreshing the token
166          try {
167            await this.auth.odswiezToken();
168            // Retry with new token
169            return this.request(endpoint, options);
170          } catch (refreshError) {
171            throw new Error('Session expired. Please log in again.');
172          }
173        }
174
175        if (!response.ok) {
176          const error = new Error(`HTTP Error: ${response.status}`);
177          error.status = response.status;
178          error.statusText = response.statusText;
179
180          // Try to get error details from response
181          try {
182            const errorData = await response.json();
183            error.data = errorData;
184            error.message = errorData.message || error.message;
185          } catch (e) {
186            // Ignore parsing errors
187          }
188
189          throw error;
190        }
191
192        // Parse response depending on type
193        const contentType = response.headers.get('content-type');
194        let data;
195
196        if (contentType && contentType.includes('application/json')) {
197          data = await response.json();
198        } else if (contentType && contentType.includes('text/')) {
199          data = await response.text();
200        } else {
201          data = await response.blob();
202        }
203
204        // Save response in cache for GET requests
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(`Request aborted after timeout (${timeout}ms)`);
215        }
216
217        throw error;
218      }
219    };
220
221    // Execute request with retries
222    return withRetry(
223      (signal) => executeRequest(signal),
224      {
225        maxAttempts: retries,
226        initialDelay: 1000,
227        delayFactor: 2,
228        timeoutMs: timeout
229      }
230    );
231  }
232
233  /**
234   * Adds a request to the sync queue (offline mode)
235   * @param {string} endpoint - API endpoint
236   * @param {Object} options - Request options
237   * @returns {Promise} - Promise with temporary result
238   */
239  addToSyncQueue(endpoint, options) {
240    const { method, body } = options;
241
242    // Generate unique operation ID
243    const operationId = `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
244
245    // Add to sync queue
246    this.syncQueue.dodajDoKolejki(
247      `${this.baseUrl}${endpoint}`,
248      {
249        method,
250        headers: {
251          'Content-Type': 'application/json',
252          // Token will be added during actual synchronization
253        },
254        body: body ? JSON.stringify(body) : undefined
255      },
256      `${method}-${endpoint}`
257    );
258
259    // Return temporary response
260    return Promise.resolve({
261      _meta: {
262        pendingSync: true,
263        operationId,
264        timestamp: new Date(),
265        offlineMode: true
266      },
267      ...body // Return passed data as temporary result
268    });
269  }
270
271  // Helper methods for different request types
272
273  /**
274   * Executes a GET request
275   */
276  async get(endpoint, options = {}) {
277    return this.request(endpoint, { ...options, method: 'GET' });
278  }
279
280  /**
281   * Executes a POST request
282   */
283  async post(endpoint, body, options = {}) {
284    return this.request(endpoint, { ...options, method: 'POST', body });
285  }
286
287  /**
288   * Executes a PUT request
289   */
290  async put(endpoint, body, options = {}) {
291    return this.request(endpoint, { ...options, method: 'PUT', body });
292  }
293
294  /**
295   * Executes a DELETE request
296   */
297  async delete(endpoint, options = {}) {
298    return this.request(endpoint, { ...options, method: 'DELETE' });
299  }
300
301  /**
302   * Uploads a file to the server
303   */
304  async uploadFile(endpoint, plik, options = {}) {
305    const {
306      nazwaPolaPliku = 'file',
307      dodatkoweDane = {},
308      naPostep = null,
309      useAuth = true
310    } = options;
311
312    // If offline, cannot upload files
313    if (!this.networkMonitor.pobierzStan().status === 'online') {
314      throw new Error('Unable to upload file in offline mode.');
315    }
316
317    // Prepare FormData
318    const formData = new FormData();
319    formData.append(nazwaPolaPliku, plik);
320
321    // Add additional data
322    Object.entries(dodatkoweDane).forEach(([klucz, wartosc]) => {
323      formData.append(klucz, JSON.stringify(wartosc));
324    });
325
326    // Request options
327    const requestOptions = {
328      method: 'POST',
329      body: formData
330    };
331
332    // Add auth token if required
333    if (useAuth) {
334      try {
335        const token = await this.auth.pobierzTokenDostepowy();
336        requestOptions.headers = {
337          'Authorization': `Bearer ${token}`
338        };
339      } catch (error) {
340        throw new Error('Not authenticated. Please log in again.');
341      }
342    }
343
344    return new Promise((resolve, reject) => {
345      const xhr = new XMLHttpRequest();
346
347      xhr.upload.addEventListener('progress', (event) => {
348        if (event.lengthComputable && typeof naPostep === 'function') {
349          const procent = Math.round((event.loaded / event.total) * 100);
350          naPostep({
351            loaded: event.loaded,
352            total: event.total,
353            procent
354          });
355        }
356      });
357
358      xhr.addEventListener('load', () => {
359        if (xhr.status >= 200 && xhr.status < 300) {
360          try {
361            const odpowiedz = JSON.parse(xhr.responseText);
362            resolve(odpowiedz);
363          } catch (e) {
364            resolve({
365              success: true,
366              status: xhr.status,
367              message: xhr.statusText
368            });
369          }
370        } else {
371          const blad = new Error(`HTTP Error: ${xhr.status}`);
372          blad.status = xhr.status;
373          blad.statusText = xhr.statusText;
374          reject(blad);
375        }
376      });
377
378      xhr.addEventListener('error', () => {
379        reject(new Error('Network error during file upload'));
380      });
381
382      xhr.addEventListener('abort', () => {
383        reject(new Error('File upload was aborted'));
384      });
385
386      xhr.open('POST', `${this.baseUrl}${endpoint}`, true);
387
388      if (requestOptions.headers) {
389        Object.entries(requestOptions.headers).forEach(([key, value]) => {
390          xhr.setRequestHeader(key, value);
391        });
392      }
393
394      xhr.send(formData);
395    });
396  }
397
398  /**
399   * Authenticates the user
400   */
401  async login(email, haslo) {
402    try {
403      const response = await this.request('/auth/login', {
404        method: 'POST',
405        body: { email, password: haslo },
406        useAuth: false
407      });
408
409      // Save tokens
410      this.auth.zapiszTokeny(response);
411
412      return {
413        success: true,
414        user: response.user
415      };
416    } catch (error) {
417      return {
418        success: false,
419        error: error.message
420      };
421    }
422  }
423
424  /**
425   * Logs the user out
426   */
427  logout() {
428    this.auth.wyloguj();
429    // Optionally clear cache on logout
430    this.cache.wyczysc();
431  }
432}
433
434// Create API client instance
435const api = new JurassicAPIClient({
436  baseUrl: 'https://api.jurassic-park.com'
437});
438
439// API client usage examples
440
441// Login
442async function logowanie(email, haslo) {
443  const result = await api.login(email, haslo);
444  if (result.success) {
445    console.log(`Zalogowano jako ${result.user.name}`);
446    return true;
447  } else {
448    console.error(`Login error: ${result.error}`);
449    return false;
450  }
451}
452
453// Fetch dinosaur data
454async function pobierzDinosaura(id) {
455  try {
456    return await api.get(`/dinosaurs/${id}`);
457  } catch (error) {
458    console.error(`Failed to fetch dinosaur #${id}:`, error.message);
459    throw error;
460  }
461}
462
463// Update dinosaur data
464async function aktualizujDinosaura(id, data) {
465  try {
466    return await api.put(`/dinosaurs/${id}`, data);
467  } catch (error) {
468    console.error(`Failed to update dinosaur #${id}:`, error.message);
469    throw error;
470  }
471}
472
473// Fetch dinosaur list with filtering
474async function pobierzListeDinosaurow(filter = {}) {
475  try {
476    return await api.get('/dinosaurs', {
477      body: filter,
478      cacheTime: 60000 // Shorter cache for lists (1 minute)
479    });
480  } catch (error) {
481    console.error('Failed to fetch dinosaurs list:', error.message);
482    throw error;
483  }
484}
485
486// Upload dinosaur medical records
487async function wyslijDokumentacjeMedyczna(dinoId, plik, data) {
488  const progressBar = document.getElementById('upload-progress');
489  const statusText = document.getElementById('upload-status');
490
491  try {
492    return await api.uploadFile(`/dinosaurs/${dinoId}/medical-records`, plik, {
493      dodatkoweDane: data,
494      naPostep: (info) => {
495        if (progressBar) progressBar.value = info.procent;
496        if (statusText) statusText.textContent = `Uploading: ${info.procent}%`;
497      }
498    });
499  } catch (error) {
500    console.error(`Failed to upload documentation for dinosaur #${dinoId}:`, error.message);
501    if (statusText) statusText.textContent = `Error: ${error.message}`;
502    throw error;
503  }
504}

In this comprehensive example, we implemented an advanced API client that handles:

  1. Comprehensive error handling — different types of HTTP and network errors
  2. Automatic request retries — for temporary problems
  3. Response caching — for faster access and offline mode support
  4. Offline operation queuing — to ensure data consistency
  5. Network state monitoring — for making appropriate decisions
  6. Authentication and token refresh — for secure API access
  7. File upload with progress monitoring — for better user experience

Such a comprehensive API client provides resilience against network issues, efficiency through caching, offline mode support, and user-friendly mechanisms like progress monitoring. It is an ideal solution for critical systems like the Jurassic Park management system, where reliability and security are paramount.

In the next lesson, we will look at how to efficiently process JSON data received from the API to present it to users in an accessible way.

Go to CodeWorlds