react-native-xenon
Version:
A powerful in-app debugging tool for React Native.
132 lines (101 loc) • 4.12 kB
text/typescript
import { NativeEventEmitter, type EmitterSubscription } from 'react-native';
import NativeWebSocketModule from 'react-native/Libraries/WebSocket/NativeWebSocketModule';
import { frozen, singleton } from '../core/utils';
import type { WebSocketHandlers } from '../types';
import { NetworkInterceptor } from './NetworkInterceptor';
const originalWebSocketConnect = NativeWebSocketModule.connect;
const originalWebSocketSend = NativeWebSocketModule.send;
const originalWebSocketSendBinary = NativeWebSocketModule.sendBinary;
const originalWebSocketClose = NativeWebSocketModule.close;
export default class WebSocketInterceptor extends NetworkInterceptor<WebSocketHandlers> {
protected handlers: WebSocketHandlers = {
connect: null,
send: null,
close: null,
onOpen: null,
onMessage: null,
onError: null,
onClose: null,
};
private eventEmitter: NativeEventEmitter | null = null;
private subscriptions: EmitterSubscription[] = [];
private readonly startTimes: Map<number, number> = new Map();
private arrayBufferToString(data?: string) {
try {
if (!data) return '(no input)';
const byteArray = Buffer.from(data, 'base64');
if (byteArray.length === 0) return '(empty array)';
return `ArrayBuffer { length: ${byteArray.length}, values: [${byteArray.join(', ')}] }`;
} catch (error) {
return `(invalid data: ${error instanceof Error ? error.message : error})`;
}
}
private registerEvents(): void {
if (!this.eventEmitter) return;
this.subscriptions = [
this.eventEmitter.addListener('websocketOpen', ev => {
const startTime = this.startTimes.get(ev.id);
const endTime = Date.now();
const duration = endTime - (startTime ?? 0);
this.startTimes.delete(ev.id);
this.handlers.onOpen?.(ev.id, duration);
}),
this.eventEmitter.addListener('websocketMessage', ev => {
this.handlers.onMessage?.(
ev.id,
ev.type === 'binary' ? this.arrayBufferToString(ev.data) : ev.data,
);
}),
this.eventEmitter.addListener('websocketClosed', ev => {
this.handlers.onClose?.(ev.id, { code: ev.code, reason: ev.reason });
}),
this.eventEmitter.addListener('websocketFailed', ev => {
this.handlers.onError?.(ev.id, { message: ev.message });
}),
];
}
private unregisterEvents() {
this.subscriptions.forEach(e => e.remove());
this.subscriptions = [];
this.eventEmitter = null;
}
enableInterception(): void {
if (this.isInterceptorEnabled) return;
this.eventEmitter = new NativeEventEmitter(NativeWebSocketModule);
this.registerEvents();
const { connectCallback, sendCallback, closeCallback } = this.getCallbacks();
const startTimes = this.startTimes;
NativeWebSocketModule.connect = function (...args) {
connectCallback?.(...args);
startTimes.set(args[3], Date.now());
originalWebSocketConnect.call(this, ...args);
};
NativeWebSocketModule.send = function (...args) {
sendCallback?.(...args);
originalWebSocketSend.call(this, ...args);
};
const arrayBufferToString = this.arrayBufferToString;
NativeWebSocketModule.sendBinary = function (base64String, socketId) {
sendCallback?.(arrayBufferToString(base64String), socketId);
originalWebSocketSendBinary.call(this, base64String, socketId);
};
NativeWebSocketModule.close = function (code, reason, socketId) {
closeCallback?.(code, reason, socketId);
originalWebSocketClose.call(this, code, reason, socketId);
};
this.isInterceptorEnabled = true;
}
disableInterception(): void {
if (!this.isInterceptorEnabled) return;
this.isInterceptorEnabled = false;
NativeWebSocketModule.connect = originalWebSocketConnect;
NativeWebSocketModule.send = originalWebSocketSend;
NativeWebSocketModule.sendBinary = originalWebSocketSendBinary;
NativeWebSocketModule.close = originalWebSocketClose;
this.clearCallbacks();
this.unregisterEvents();
}
}