| 
									
										
										
											
												feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly.
- Introduced InterceptorManager for managing request, response, and error interceptors.
- Developed RetryManager for handling request retries with customizable backoff strategies.
- Implemented RequestDeduplicator to prevent simultaneous identical requests.
- Created timeout utilities for handling request timeouts.
- Enhanced WebrequestClient to support global interceptors, caching, and retry logic.
- Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling.
- Established a fetch-compatible webrequest function for seamless integration.
- Defined core type structures for caching, retry options, interceptors, and web request configurations.
											
										 
											2025-10-20 09:59:24 +00:00
										 |  |  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | 
					
						
							|  |  |  | import { webrequest, WebrequestClient } from '../ts/index.js'; | 
					
						
							|  |  |  | import * as typedserver from '@api.global/typedserver'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | let testServer: typedserver.servertools.Server; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Setup test server
 | 
					
						
							|  |  |  | tap.test('setup test server for v4 tests', async () => { | 
					
						
							|  |  |  |   testServer = new typedserver.servertools.Server({ | 
					
						
							|  |  |  |     cors: false, | 
					
						
							|  |  |  |     forceSsl: false, | 
					
						
							|  |  |  |     port: 2346, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Route that returns JSON with cache headers
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/cached', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', (req, res) => { | 
					
						
							|  |  |  |       res.setHeader('Cache-Control', 'max-age=60'); | 
					
						
							|  |  |  |       res.setHeader('ETag', '"12345"'); | 
					
						
							|  |  |  |       res.status(200); | 
					
						
							|  |  |  |       res.send({ data: 'cached response', timestamp: Date.now() }); | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Route that returns different data each time
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/dynamic', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', (req, res) => { | 
					
						
							|  |  |  |       res.status(200); | 
					
						
							|  |  |  |       res.send({ data: 'dynamic response', timestamp: Date.now() }); | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Route that sometimes fails (for retry testing)
 | 
					
						
							|  |  |  |   let requestCount = 0; | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/flaky', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', (req, res) => { | 
					
						
							|  |  |  |       requestCount++; | 
					
						
							|  |  |  |       if (requestCount < 3) { | 
					
						
							|  |  |  |         res.status(500); | 
					
						
							|  |  |  |         res.end(); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         res.status(200); | 
					
						
							|  |  |  |         res.send({ success: true, attempts: requestCount }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-20 13:41:03 +00:00
										 |  |  |   // Route that always fails with 500 (for fallback testing)
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/always-fails', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', (req, res) => { | 
					
						
							|  |  |  |       res.status(500); | 
					
						
							|  |  |  |       res.end(); | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Route that takes a long time to respond (for timeout testing)
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/slow', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', async (req, res) => { | 
					
						
							|  |  |  |       await new Promise((resolve) => setTimeout(resolve, 5000)); | 
					
						
							|  |  |  |       res.status(200); | 
					
						
							|  |  |  |       res.send({ data: 'slow response' }); | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
											
												feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly.
- Introduced InterceptorManager for managing request, response, and error interceptors.
- Developed RetryManager for handling request retries with customizable backoff strategies.
- Implemented RequestDeduplicator to prevent simultaneous identical requests.
- Created timeout utilities for handling request timeouts.
- Enhanced WebrequestClient to support global interceptors, caching, and retry logic.
- Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling.
- Established a fetch-compatible webrequest function for seamless integration.
- Defined core type structures for caching, retry options, interceptors, and web request configurations.
											
										 
											2025-10-20 09:59:24 +00:00
										 |  |  |   // Route that returns 304 when ETag matches
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/conditional', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('GET', (req, res) => { | 
					
						
							|  |  |  |       const ifNoneMatch = req.headers['if-none-match']; | 
					
						
							|  |  |  |       if (ifNoneMatch === '"67890"') { | 
					
						
							|  |  |  |         res.status(304); | 
					
						
							|  |  |  |         res.end(); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         res.setHeader('ETag', '"67890"'); | 
					
						
							|  |  |  |         res.status(200); | 
					
						
							|  |  |  |         res.send({ data: 'conditional response' }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // POST route for testing
 | 
					
						
							|  |  |  |   testServer.addRoute( | 
					
						
							|  |  |  |     '/post', | 
					
						
							|  |  |  |     new typedserver.servertools.Handler('POST', (req, res) => { | 
					
						
							|  |  |  |       res.status(200); | 
					
						
							|  |  |  |       res.send({ received: true }); | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   await testServer.start(); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 1: Basic fetch-compatible API
 | 
					
						
							|  |  |  | tap.test('should work as fetch replacement', async () => { | 
					
						
							|  |  |  |   const response = await webrequest('http://localhost:2346/dynamic'); | 
					
						
							|  |  |  |   expect(response).toBeInstanceOf(Response); | 
					
						
							|  |  |  |   expect(response.ok).toEqual(true); | 
					
						
							|  |  |  |   expect(response.status).toEqual(200); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const data = await response.json(); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('data'); | 
					
						
							|  |  |  |   expect(data.data).toEqual('dynamic response'); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 2: getJson convenience method with generics
 | 
					
						
							|  |  |  | tap.test('should support getJson with type safety', async () => { | 
					
						
							|  |  |  |   interface TestData { | 
					
						
							|  |  |  |     data: string; | 
					
						
							|  |  |  |     timestamp: number; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const data = await webrequest.getJson<TestData>('http://localhost:2346/dynamic'); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('data'); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('timestamp'); | 
					
						
							|  |  |  |   expect(typeof data.timestamp).toEqual('number'); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 3: POST with JSON
 | 
					
						
							|  |  |  | tap.test('should support postJson', async () => { | 
					
						
							|  |  |  |   const data = await webrequest.postJson('http://localhost:2346/post', { | 
					
						
							|  |  |  |     test: 'data' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('received'); | 
					
						
							|  |  |  |   expect(data.received).toEqual(true); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 4: Cache strategy - network-first
 | 
					
						
							|  |  |  | tap.test('should support network-first cache strategy', async () => { | 
					
						
							|  |  |  |   const response1 = await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'network-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   const data1 = await response1.json(); | 
					
						
							|  |  |  |   expect(data1).toHaveProperty('timestamp'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Second request should hit network but may use cache on error
 | 
					
						
							|  |  |  |   const response2 = await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'network-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   const data2 = await response2.json(); | 
					
						
							|  |  |  |   expect(data2).toHaveProperty('timestamp'); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 5: Cache strategy - cache-first
 | 
					
						
							|  |  |  | tap.test('should support cache-first strategy', async () => { | 
					
						
							|  |  |  |   // First request goes to network
 | 
					
						
							|  |  |  |   const response1 = await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'cache-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   const data1 = await response1.json(); | 
					
						
							|  |  |  |   const timestamp1 = data1.timestamp; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Second request should use cache
 | 
					
						
							|  |  |  |   const response2 = await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'cache-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   const data2 = await response2.json(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Timestamps should be identical if cached
 | 
					
						
							|  |  |  |   expect(data2.timestamp).toEqual(timestamp1); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 6: Retry system
 | 
					
						
							|  |  |  | tap.test('should retry failed requests', async () => { | 
					
						
							|  |  |  |   const response = await webrequest('http://localhost:2346/flaky', { | 
					
						
							|  |  |  |     retry: { | 
					
						
							|  |  |  |       maxAttempts: 3, | 
					
						
							|  |  |  |       backoff: 'constant', | 
					
						
							|  |  |  |       initialDelay: 100 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const data = await response.json(); | 
					
						
							|  |  |  |   expect(data.success).toEqual(true); | 
					
						
							|  |  |  |   expect(data.attempts).toBeGreaterThanOrEqual(3); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 7: Fallback URLs
 | 
					
						
							|  |  |  | tap.test('should support fallback URLs', async () => { | 
					
						
							| 
									
										
										
										
											2025-10-20 13:41:03 +00:00
										 |  |  |   const response = await webrequest('http://localhost:2346/always-fails', { | 
					
						
							| 
									
										
										
											
												feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly.
- Introduced InterceptorManager for managing request, response, and error interceptors.
- Developed RetryManager for handling request retries with customizable backoff strategies.
- Implemented RequestDeduplicator to prevent simultaneous identical requests.
- Created timeout utilities for handling request timeouts.
- Enhanced WebrequestClient to support global interceptors, caching, and retry logic.
- Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling.
- Established a fetch-compatible webrequest function for seamless integration.
- Defined core type structures for caching, retry options, interceptors, and web request configurations.
											
										 
											2025-10-20 09:59:24 +00:00
										 |  |  |     fallbackUrls: ['http://localhost:2346/dynamic'], | 
					
						
							|  |  |  |     retry: { | 
					
						
							|  |  |  |       maxAttempts: 2 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   expect(response.ok).toEqual(true); | 
					
						
							|  |  |  |   const data = await response.json(); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('data'); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 8: Request interceptors
 | 
					
						
							|  |  |  | tap.test('should support request interceptors', async () => { | 
					
						
							|  |  |  |   let interceptorCalled = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const response = await webrequest('http://localhost:2346/dynamic', { | 
					
						
							|  |  |  |     interceptors: { | 
					
						
							|  |  |  |       request: [(req) => { | 
					
						
							|  |  |  |         interceptorCalled = true; | 
					
						
							|  |  |  |         const headers = new Headers(req.headers); | 
					
						
							|  |  |  |         headers.set('X-Custom-Header', 'test'); | 
					
						
							|  |  |  |         return new Request(req, { headers }); | 
					
						
							|  |  |  |       }] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   expect(interceptorCalled).toEqual(true); | 
					
						
							|  |  |  |   expect(response.ok).toEqual(true); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 9: Response interceptors
 | 
					
						
							|  |  |  | tap.test('should support response interceptors', async () => { | 
					
						
							|  |  |  |   let interceptorCalled = false; | 
					
						
							|  |  |  |   let capturedStatus: number; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const response = await webrequest('http://localhost:2346/dynamic', { | 
					
						
							|  |  |  |     interceptors: { | 
					
						
							|  |  |  |       response: [(res) => { | 
					
						
							|  |  |  |         interceptorCalled = true; | 
					
						
							|  |  |  |         capturedStatus = res.status; | 
					
						
							|  |  |  |         return res; | 
					
						
							|  |  |  |       }] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   expect(interceptorCalled).toEqual(true); | 
					
						
							|  |  |  |   expect(capturedStatus!).toEqual(200); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 10: Global interceptors with WebrequestClient
 | 
					
						
							|  |  |  | tap.test('should support global interceptors', async () => { | 
					
						
							|  |  |  |   const client = new WebrequestClient(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let globalInterceptorCalled = false; | 
					
						
							|  |  |  |   client.addRequestInterceptor((req) => { | 
					
						
							|  |  |  |     globalInterceptorCalled = true; | 
					
						
							|  |  |  |     return req; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   await client.request('http://localhost:2346/dynamic'); | 
					
						
							|  |  |  |   expect(globalInterceptorCalled).toEqual(true); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 11: Request deduplication
 | 
					
						
							|  |  |  | tap.test('should deduplicate simultaneous requests', async () => { | 
					
						
							|  |  |  |   const start = Date.now(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Make 3 identical requests simultaneously
 | 
					
						
							|  |  |  |   const [res1, res2, res3] = await Promise.all([ | 
					
						
							|  |  |  |     webrequest('http://localhost:2346/dynamic', { deduplicate: true }), | 
					
						
							|  |  |  |     webrequest('http://localhost:2346/dynamic', { deduplicate: true }), | 
					
						
							|  |  |  |     webrequest('http://localhost:2346/dynamic', { deduplicate: true }), | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const [data1, data2, data3] = await Promise.all([ | 
					
						
							|  |  |  |     res1.json(), | 
					
						
							|  |  |  |     res2.json(), | 
					
						
							|  |  |  |     res3.json(), | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // All should have the same timestamp (same response)
 | 
					
						
							|  |  |  |   expect(data1.timestamp).toEqual(data2.timestamp); | 
					
						
							|  |  |  |   expect(data2.timestamp).toEqual(data3.timestamp); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 12: Timeout
 | 
					
						
							|  |  |  | tap.test('should support timeout', async () => { | 
					
						
							|  |  |  |   try { | 
					
						
							| 
									
										
										
										
											2025-10-20 13:41:03 +00:00
										 |  |  |     await webrequest('http://localhost:2346/slow', { | 
					
						
							|  |  |  |       timeout: 100 // 100ms timeout should fail (route takes 5000ms)
 | 
					
						
							| 
									
										
										
											
												feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly.
- Introduced InterceptorManager for managing request, response, and error interceptors.
- Developed RetryManager for handling request retries with customizable backoff strategies.
- Implemented RequestDeduplicator to prevent simultaneous identical requests.
- Created timeout utilities for handling request timeouts.
- Enhanced WebrequestClient to support global interceptors, caching, and retry logic.
- Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling.
- Established a fetch-compatible webrequest function for seamless integration.
- Defined core type structures for caching, retry options, interceptors, and web request configurations.
											
										 
											2025-10-20 09:59:24 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |     throw new Error('Should have timed out'); | 
					
						
							|  |  |  |   } catch (error) { | 
					
						
							|  |  |  |     expect(error.message).toContain('timeout'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 13: WebrequestClient with default options
 | 
					
						
							|  |  |  | tap.test('should support WebrequestClient with defaults', async () => { | 
					
						
							|  |  |  |   const client = new WebrequestClient({ | 
					
						
							|  |  |  |     logging: false, | 
					
						
							|  |  |  |     cacheStrategy: 'network-first', | 
					
						
							|  |  |  |     timeout: 30000 | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const data = await client.getJson('http://localhost:2346/dynamic'); | 
					
						
							|  |  |  |   expect(data).toHaveProperty('data'); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Test 14: Clear cache
 | 
					
						
							|  |  |  | tap.test('should clear cache', async () => { | 
					
						
							|  |  |  |   // Cache a request
 | 
					
						
							|  |  |  |   await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'cache-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Clear cache
 | 
					
						
							|  |  |  |   await webrequest.clearCache(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // This should work even though cache is cleared
 | 
					
						
							|  |  |  |   const response = await webrequest('http://localhost:2346/cached', { | 
					
						
							|  |  |  |     cacheStrategy: 'cache-first' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   expect(response.ok).toEqual(true); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Cleanup
 | 
					
						
							|  |  |  | tap.test('stop test server', async () => { | 
					
						
							|  |  |  |   await testServer.stop(); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default tap.start(); |