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.
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}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}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}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}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}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}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}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}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:
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.