| |
| |
| |
|
|
| |
| jest.mock('react', () => ({ |
| cache: <T extends (...args: any[]) => any>(fn: T): T => { |
| const cache = new Map<string, ReturnType<T>>() |
| return ((...args: Parameters<T>) => { |
| const key = JSON.stringify(args) |
| if (!cache.has(key)) { |
| cache.set(key, fn(...args)) |
| } |
| return cache.get(key)! |
| }) as T |
| }, |
| })) |
|
|
| |
| jest.mock('./clone-response', () => ({ |
| cloneResponse: (response: Response) => { |
| |
| const clone1 = response.clone() |
| const clone2 = response.clone() |
| return [clone1, clone2] |
| }, |
| })) |
|
|
| import { createDedupeFetch } from './dedupe-fetch' |
|
|
| describe('dedupe-fetch', () => { |
| let originalFetch: jest.MockedFunction<typeof fetch> |
| let dedupeFetch: ReturnType<typeof createDedupeFetch> |
|
|
| beforeEach(() => { |
| |
| originalFetch = jest.fn() |
| dedupeFetch = createDedupeFetch(originalFetch) |
|
|
| |
| jest.clearAllMocks() |
| }) |
|
|
| describe('deduplication behavior', () => { |
| it('should dedupe identical GET requests', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(originalFetch).toHaveBeenCalledWith( |
| 'https://example.com/api', |
| undefined |
| ) |
|
|
| |
| expect(await response1.text()).toBe('test response') |
| expect(await response2.text()).toBe('test response') |
| }) |
|
|
| it('should dedupe identical HEAD requests', async () => { |
| const mockResponse = new Response(null, { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| method: 'HEAD', |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| method: 'HEAD', |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| }) |
|
|
| it('should not dedupe requests with different URLs', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api1') |
| const promise2 = dedupeFetch('https://example.com/api2') |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| expect(await response1.text()).toBe('response 1') |
| expect(await response2.text()).toBe('response 2') |
| }) |
|
|
| it('should not dedupe requests with different headers', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| headers: { 'X-Custom': 'value1' }, |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| headers: { 'X-Custom': 'value2' }, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should dedupe requests with different traceparent headers', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| }, |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', |
| }, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(originalFetch).toHaveBeenNthCalledWith( |
| 1, |
| 'https://example.com/api', |
| expect.objectContaining({ |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| }, |
| }) |
| ) |
| }) |
|
|
| it('should dedupe requests with different tracestate headers', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| tracestate: 'vendor1=value1,vendor2=value2', |
| }, |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| tracestate: 'vendor1=value3,vendor2=value4', |
| }, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(originalFetch).toHaveBeenNthCalledWith( |
| 1, |
| 'https://example.com/api', |
| expect.objectContaining({ |
| headers: { |
| traceparent: |
| '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', |
| tracestate: 'vendor1=value1,vendor2=value2', |
| }, |
| }) |
| ) |
| }) |
|
|
| it('should not dedupe requests with different request modes', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| mode: 'cors', |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| mode: 'no-cors', |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should handle multiple sequential duplicate requests', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
| const promise3 = dedupeFetch('https://example.com/api') |
|
|
| const [response1, response2, response3] = await Promise.all([ |
| promise1, |
| promise2, |
| promise3, |
| ]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
|
|
| |
| expect(await response1.text()).toBe('test response') |
| expect(await response2.text()).toBe('test response') |
| expect(await response3.text()).toBe('test response') |
| }) |
| }) |
|
|
| describe('signal handling', () => { |
| it('should not dedupe requests with signals', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| const controller1 = new AbortController() |
| const controller2 = new AbortController() |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| signal: controller1.signal, |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| signal: controller2.signal, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should pass signal through to original fetch', async () => { |
| const mockResponse = new Response('response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const controller = new AbortController() |
|
|
| await dedupeFetch('https://example.com/api', { |
| signal: controller.signal, |
| }) |
|
|
| expect(originalFetch).toHaveBeenCalledWith( |
| 'https://example.com/api', |
| expect.objectContaining({ signal: controller.signal }) |
| ) |
| }) |
| }) |
|
|
| describe('method handling', () => { |
| it('should not dedupe POST requests', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| method: 'POST', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| method: 'POST', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should not dedupe PUT requests', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| method: 'PUT', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| method: 'PUT', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should not dedupe DELETE requests', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| method: 'DELETE', |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| method: 'DELETE', |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should not dedupe PATCH requests', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| method: 'PATCH', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| method: 'PATCH', |
| body: JSON.stringify({ data: 'test' }), |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
| }) |
|
|
| describe('keepalive handling', () => { |
| it('should not dedupe requests with keepalive', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| keepalive: true, |
| }) |
| const promise2 = dedupeFetch('https://example.com/api', { |
| keepalive: true, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
| }) |
|
|
| describe('Request object handling', () => { |
| it('should handle Request objects as input', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const request = new Request('https://example.com/api', { |
| method: 'GET', |
| headers: { 'X-Custom': 'value' }, |
| }) |
|
|
| const response = await dedupeFetch(request) |
|
|
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(await response.text()).toBe('test response') |
| }) |
|
|
| it('should dedupe identical Request objects', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const request1 = new Request('https://example.com/api') |
| const request2 = new Request('https://example.com/api') |
|
|
| const promise1 = dedupeFetch(request1) |
| const promise2 = dedupeFetch(request2) |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(await response1.text()).toBe('test response') |
| expect(await response2.text()).toBe('test response') |
| }) |
|
|
| it('should not dedupe Request objects with different properties', async () => { |
| const mockResponse1 = new Response('response 1', { status: 200 }) |
| const mockResponse2 = new Response('response 2', { status: 200 }) |
|
|
| originalFetch |
| .mockResolvedValueOnce(mockResponse1) |
| .mockResolvedValueOnce(mockResponse2) |
|
|
| const request1 = new Request('https://example.com/api', { |
| headers: { 'X-Custom': 'value1' }, |
| }) |
| const request2 = new Request('https://example.com/api', { |
| headers: { 'X-Custom': 'value2' }, |
| }) |
|
|
| const promise1 = dedupeFetch(request1) |
| const promise2 = dedupeFetch(request2) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(2) |
| }) |
|
|
| it('should preserve the Request object body when it is a ReadableStream', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const stream = new ReadableStream({ |
| start(controller) { |
| controller.enqueue(new TextEncoder().encode('stream data')) |
| controller.close() |
| }, |
| }) |
|
|
| const request = new Request('https://example.com/api', { |
| method: 'POST', |
| body: stream, |
| |
| duplex: 'half', |
| }) |
|
|
| await dedupeFetch(request) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledWith(request, undefined) |
| }) |
| }) |
|
|
| describe('URL handling', () => { |
| it('should handle URL objects as input', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const url = new URL('https://example.com/api') |
| const response = await dedupeFetch(url) |
|
|
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(await response.text()).toBe('test response') |
| }) |
|
|
| it('should dedupe identical URL objects', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const url1 = new URL('https://example.com/api') |
| const url2 = new URL('https://example.com/api') |
|
|
| const promise1 = dedupeFetch(url1) |
| const promise2 = dedupeFetch(url2) |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| expect(await response1.text()).toBe('test response') |
| expect(await response2.text()).toBe('test response') |
| }) |
| }) |
|
|
| describe('cache key generation', () => { |
| it('should generate consistent cache keys for identical requests', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api', { |
| headers: { |
| 'X-Header-A': 'value-a', |
| 'X-Header-B': 'value-b', |
| }, |
| mode: 'cors', |
| credentials: 'include', |
| }) |
|
|
| const promise2 = dedupeFetch('https://example.com/api', { |
| credentials: 'include', |
| mode: 'cors', |
| headers: { |
| 'X-Header-A': 'value-a', |
| 'X-Header-B': 'value-b', |
| }, |
| }) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| }) |
|
|
| it('should use simple cache key for string URL without options', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| |
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| }) |
| }) |
|
|
| describe('error handling', () => { |
| it('should propagate fetch errors', async () => { |
| const error = new Error('Network error') |
| originalFetch.mockRejectedValue(error) |
|
|
| await expect(dedupeFetch('https://example.com/api')).rejects.toThrow( |
| 'Network error' |
| ) |
| }) |
|
|
| it('should share errors between deduped requests', async () => { |
| const error = new Error('Network error') |
| originalFetch.mockRejectedValue(error) |
|
|
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
|
|
| await expect(promise1).rejects.toThrow('Network error') |
| await expect(promise2).rejects.toThrow('Network error') |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| }) |
| }) |
|
|
| describe('response cloning', () => { |
| it('should allow multiple consumers to read the response body', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| const text1 = await response1.text() |
| const text2 = await response2.text() |
|
|
| expect(text1).toBe('test response') |
| expect(text2).toBe('test response') |
| }) |
|
|
| it('should preserve response properties when cloning', async () => { |
| const headers = new Headers({ |
| 'Content-Type': 'application/json', |
| 'X-Custom': 'value', |
| }) |
|
|
| const mockResponse = new Response('{"data": "test"}', { |
| status: 201, |
| statusText: 'Created', |
| headers, |
| }) |
|
|
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const promise1 = dedupeFetch('https://example.com/api') |
| const promise2 = dedupeFetch('https://example.com/api') |
|
|
| const [response1, response2] = await Promise.all([promise1, promise2]) |
|
|
| |
| expect(response1.status).toBe(201) |
| expect(response1.statusText).toBe('Created') |
| expect(response1.headers.get('Content-Type')).toBe('application/json') |
| expect(response1.headers.get('X-Custom')).toBe('value') |
|
|
| expect(response2.status).toBe(201) |
| expect(response2.statusText).toBe('Created') |
| expect(response2.headers.get('Content-Type')).toBe('application/json') |
| expect(response2.headers.get('X-Custom')).toBe('value') |
| }) |
| }) |
|
|
| describe('request options', () => { |
| it('should handle various request options', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const options: RequestInit = { |
| method: 'GET', |
| headers: { 'X-Custom': 'value' }, |
| mode: 'cors', |
| credentials: 'include', |
| redirect: 'follow', |
| referrer: 'https://referrer.com', |
| referrerPolicy: 'no-referrer', |
| integrity: 'sha256-abc123', |
| } |
|
|
| await dedupeFetch('https://example.com/api', options) |
|
|
| expect(originalFetch).toHaveBeenCalledWith( |
| 'https://example.com/api', |
| options |
| ) |
| }) |
|
|
| it('should dedupe requests with identical complex options', async () => { |
| const mockResponse = new Response('test response', { status: 200 }) |
| originalFetch.mockResolvedValue(mockResponse) |
|
|
| const options: RequestInit = { |
| headers: { 'X-Custom': 'value', Authorization: 'Bearer token' }, |
| mode: 'cors', |
| credentials: 'include', |
| redirect: 'manual', |
| referrer: 'https://referrer.com', |
| referrerPolicy: 'strict-origin', |
| integrity: 'sha256-abc123', |
| } |
|
|
| const promise1 = dedupeFetch('https://example.com/api', options) |
| const promise2 = dedupeFetch('https://example.com/api', options) |
|
|
| await Promise.all([promise1, promise2]) |
|
|
| |
| expect(originalFetch).toHaveBeenCalledTimes(1) |
| }) |
| }) |
| }) |
|
|