import { createNanoEvents } from 'nanoevents'; import { getNetworkRequestsRegistry } from './network-requests-registry'; import { XHRInterceptor } from './xhr-interceptor'; import { getRequestBody, getResponseSize, getInitiatorFromStack, setupRequestOverride, } from './http-utils'; import { applyReactNativeResponseHeadersLogic } from '../../utils/applyReactNativeResponseHeadersLogic'; import { getContentType } from '../utils'; import { getOverridesRegistry } from './overrides-registry'; import type { HttpEventMap, HttpMethod } from '../../shared/http-events'; import type { Inspector } from '../inspector'; // HTTP-specific event map for the inspector export type { HttpEventMap }; export const HTTP_EVENTS: (keyof HttpEventMap)[] = [ 'request-sent', 'response-received', 'request-completed', 'request-failed', 'request-progress', ]; export const isHttpEvent = (type: string): type is keyof HttpEventMap => { return (HTTP_EVENTS as readonly string[]).includes(type); }; type NanoEventsMap = { [K in keyof HttpEventMap]: (data: HttpEventMap[K]) => void; }; export type HTTPInspector = Inspector & { getNetworkRequestsRegistry: () => ReturnType< typeof getNetworkRequestsRegistry >; }; const READY_STATE_HEADERS_RECEIVED = 2; export const getHTTPInspector = (): HTTPInspector => { const eventEmitter = createNanoEvents(); const networkRequestsRegistry = getNetworkRequestsRegistry(); const overridesRegistry = getOverridesRegistry(); XHRInterceptor.setOverrideCallback((request) => setupRequestOverride(overridesRegistry, request), ); return { enable: () => { if (XHRInterceptor.isInterceptorEnabled()) return; XHRInterceptor.disableInterception(); XHRInterceptor.setSendCallback((data, request) => { const initiator = getInitiatorFromStack(); const sendTime = Date.now(); const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; request._rozeniteRequestId = requestId; networkRequestsRegistry.addEntry(requestId, request); let ttfb = 0; eventEmitter.emit('request-sent', { requestId: requestId, timestamp: sendTime, request: { url: request._url as string, method: request._method as HttpMethod, headers: request._headers, postData: getRequestBody(data), }, type: 'XHR', initiator, source: 'builtin', }); request.addEventListener('readystatechange', () => { if (request.readyState === READY_STATE_HEADERS_RECEIVED) { ttfb = Date.now() - sendTime; } }); request.addEventListener('progress', (event) => { eventEmitter.emit('request-progress', { requestId: requestId, timestamp: Date.now(), loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, source: 'builtin', }); }); request.addEventListener('load', () => { eventEmitter.emit('response-received', { requestId: requestId, timestamp: Date.now(), type: 'XHR', response: { url: request._url as string, status: request.status, statusText: request.statusText, headers: applyReactNativeResponseHeadersLogic( request.responseHeaders || {}, ), contentType: getContentType(request), size: getResponseSize(request), responseTime: Date.now(), }, source: 'builtin', }); }); request.addEventListener('loadend', () => { eventEmitter.emit('request-completed', { requestId: requestId, timestamp: Date.now(), duration: Date.now() - sendTime, size: getResponseSize(request), ttfb, source: 'builtin', }); }); request.addEventListener('error', () => { eventEmitter.emit('request-failed', { requestId: requestId, timestamp: Date.now(), type: 'XHR', error: 'Failed', canceled: false, source: 'builtin', }); }); request.addEventListener('abort', () => { eventEmitter.emit('request-failed', { requestId: requestId, timestamp: Date.now(), type: 'XHR', error: 'Aborted', canceled: true, source: 'builtin', }); }); request.addEventListener('timeout', () => { eventEmitter.emit('request-failed', { requestId: requestId, timestamp: Date.now(), type: 'XHR', error: 'Timeout', canceled: false, source: 'builtin', }); }); }); XHRInterceptor.enableInterception(); }, disable: () => { XHRInterceptor.disableInterception(); }, isEnabled: () => { return XHRInterceptor.isInterceptorEnabled(); }, dispose: () => { XHRInterceptor.disableInterception(); networkRequestsRegistry.clear(); }, getNetworkRequestsRegistry: () => networkRequestsRegistry, on: ( event: TEventType, callback: (data: HttpEventMap[TEventType]) => void, ) => eventEmitter.on(event, callback as NanoEventsMap[TEventType]), }; };