Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rozenite/network-activity-plugin': patch
---

Replace the private `react-native/Libraries/WebSocket/WebSocketInterceptor` import with a self-contained implementation built on the public `TurboModuleRegistry` and `NativeEventEmitter` APIs, so the plugin no longer depends on React Native's internal module paths (compatible with [Strict TypeScript API](https://reactnative.dev/docs/strict-typescript-api)).
1 change: 1 addition & 0 deletions packages/network-activity-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@rozenite/agent-bridge": "workspace:*",
"@rozenite/agent-shared": "workspace:*",
"@rozenite/plugin-bridge": "workspace:*",
"base64-js": "^1.5.1",
"nanoevents": "^9.1.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TurboModuleRegistry } from 'react-native';

/**
* Mirrors React Native's internal `WebSocketModule` TurboModule spec
* (`react-native/src/private/specs_DEPRECATED/modules/NativeWebSocketModule`).
* Looked up by name via the public `TurboModuleRegistry` API instead of
* importing the private spec module directly.
*/
export interface NativeWebSocketModule {
getConstants?(): Record<string, unknown>;
connect(
url: string,
protocols: string[] | null,
options: { headers?: Record<string, unknown> },
socketId: number,
): void;
send(message: string, forSocketID: number): void;
sendBinary(base64String: string, forSocketID: number): void;
ping(socketID: number): void;
close(...args: [code: number, reason: string, socketID: number] | [socketID: number]): void;
addListener(eventName: string): void;
removeListeners(count: number): void;
}

export const getNativeWebSocketModule = (): NativeWebSocketModule => {
return TurboModuleRegistry.getEnforcing<NativeWebSocketModule>('WebSocketModule');
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Platform } from 'react-native';
import WebSocketInterceptor from 'react-native/Libraries/WebSocket/WebSocketInterceptor';
import { NativeEventEmitter } from 'react-native';
import base64 from 'base64-js';
import { getNativeWebSocketModule } from './native-websocket-module';

export interface WebSocketInterceptor {
/**
Expand Down Expand Up @@ -53,88 +54,159 @@ export interface WebSocketInterceptor {
disableInterception(): void;
}

export interface WebSocketInterceptorPreRN079 {
/**
* Invoked when RCTWebSocketModule.close(...) is called.
*/
setCloseCallback(
callback: (code: number | null, reason: string | null, socketId: number) => void,
): void;

/**
* Invoked when RCTWebSocketModule.send(...) or sendBinary(...) is called.
*/
setSendCallback(callback: (data: string, socketId: number) => void): void;

/**
* Invoked when RCTWebSocketModule.connect(...) is called.
*/
setConnectCallback(
callback: (
url: string,
protocols: string[] | null,
options: string[],
socketId: number,
) => void,
): void;

/**
* Invoked when event "websocketOpen" happens.
*/
setOnOpenCallback(callback: (socketId: number) => void): void;

/**
* Invoked when event "websocketMessage" happens.
*/
setOnMessageCallback(callback: (socketId: number, data: string) => void): void;

/**
* Invoked when event "websocketFailed" happens.
*/
setOnErrorCallback(callback: (socketId: number, error: string) => void): void;

/**
* Invoked when event "websocketClosed" happens.
*/
setOnCloseCallback(
callback: (socketId: number, error: { code: number; reason?: string }) => void,
): void;

isInterceptorEnabled(): boolean;
enableInterception(): void;
disableInterception(): void;
}

/**
* A network interceptor which monkey-patches the native WebSocket module to
* gather all WebSocket network events.
*
* Vendored from React Native's internal (and now deprecated)
* `Libraries/WebSocket/WebSocketInterceptor.js`, rebuilt on top of the
* public `TurboModuleRegistry` / `NativeEventEmitter` APIs so this package
* has no dependency on React Native's private module paths.
* @see https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js
*/
export const getWebSocketInterceptor = (): WebSocketInterceptor => {
/**
* Note: RN 0.79 changed the order of the arguments.
* @see https://github.com/facebook/react-native/commit/d2adb976abebcb0f38750903d98fbb5a3f50924b
*/

if (Platform.constants.reactNativeVersion.minor >= 79) {
return WebSocketInterceptor as WebSocketInterceptor;
} else {
const WebSocketInterceptorPreRN079 = WebSocketInterceptor as WebSocketInterceptorPreRN079;

return {
...WebSocketInterceptorPreRN079,
setOnMessageCallback: (callback: (data: string, socketId: number) => void) => {
WebSocketInterceptorPreRN079.setOnMessageCallback((socketId, data) => {
callback(data, socketId);
});
},
setOnCloseCallback: (
callback: (error: { code: number; reason?: string }, socketId: number) => void,
) => {
WebSocketInterceptorPreRN079.setOnCloseCallback((error, socketId) => {
callback(socketId, error);
});
},
setOnErrorCallback: (callback: (error: string, socketId: number) => void) => {
WebSocketInterceptorPreRN079.setOnErrorCallback((error, socketId) => {
callback(socketId, error);
});
},
} as WebSocketInterceptor;
}
const nativeWebSocketModule = getNativeWebSocketModule();

const originalConnect = nativeWebSocketModule.connect.bind(nativeWebSocketModule);
const originalSend = nativeWebSocketModule.send.bind(nativeWebSocketModule);
const originalSendBinary = nativeWebSocketModule.sendBinary.bind(nativeWebSocketModule);
const originalClose = nativeWebSocketModule.close.bind(nativeWebSocketModule);

let eventEmitter: NativeEventEmitter | undefined;
let subscriptions: { remove(): void }[] = [];

let closeCallback: Parameters<WebSocketInterceptor['setCloseCallback']>[0] | null = null;
let sendCallback: Parameters<WebSocketInterceptor['setSendCallback']>[0] | null = null;
let connectCallback: Parameters<WebSocketInterceptor['setConnectCallback']>[0] | null = null;
let onOpenCallback: Parameters<WebSocketInterceptor['setOnOpenCallback']>[0] | null = null;
let onMessageCallback: Parameters<WebSocketInterceptor['setOnMessageCallback']>[0] | null = null;
let onErrorCallback: Parameters<WebSocketInterceptor['setOnErrorCallback']>[0] | null = null;
let onCloseCallback: Parameters<WebSocketInterceptor['setOnCloseCallback']>[0] | null = null;

let isInterceptorEnabled = false;

const arrayBufferToString = (data: string): string => {
const value = base64.toByteArray(data).buffer;
return `ArrayBuffer {${Array.from(new Uint8Array(value)).join(',')}}`;
};

const registerEvents = () => {
if (!eventEmitter) {
return;
}

subscriptions = [
eventEmitter.addListener(
'websocketMessage',
(ev: { type?: 'binary' | 'text'; data: string; id: number }) => {
onMessageCallback?.(
ev.type === 'binary' ? arrayBufferToString(ev.data) : ev.data,
ev.id,
);
},
),
eventEmitter.addListener('websocketOpen', (ev: { id: number }) => {
onOpenCallback?.(ev.id);
}),
eventEmitter.addListener(
'websocketClosed',
(ev: { code: number; reason?: string; id: number }) => {
onCloseCallback?.({ code: ev.code, reason: ev.reason }, ev.id);
},
),
eventEmitter.addListener('websocketFailed', (ev: { message: string; id: number }) => {
onErrorCallback?.(ev.message, ev.id);
}),
];
};

const unregisterEvents = () => {
subscriptions.forEach((subscription) => subscription.remove());
subscriptions = [];
};

return {
setCloseCallback(callback) {
closeCallback = callback;
},
setSendCallback(callback) {
sendCallback = callback;
},
setConnectCallback(callback) {
connectCallback = callback;
},
setOnOpenCallback(callback) {
onOpenCallback = callback;
},
setOnMessageCallback(callback) {
onMessageCallback = callback;
},
setOnErrorCallback(callback) {
onErrorCallback = callback;
},
setOnCloseCallback(callback) {
onCloseCallback = callback;
},
isInterceptorEnabled() {
return isInterceptorEnabled;
},
enableInterception() {
if (isInterceptorEnabled) {
return;
}

eventEmitter = new NativeEventEmitter(nativeWebSocketModule);
registerEvents();

nativeWebSocketModule.connect = (url, protocols, options, socketId) => {
connectCallback?.(url, protocols, options as unknown as string[], socketId);
return originalConnect(url, protocols, options, socketId);
};

nativeWebSocketModule.send = (data, socketId) => {
sendCallback?.(data, socketId);
return originalSend(data, socketId);
};

nativeWebSocketModule.sendBinary = (data, socketId) => {
sendCallback?.(arrayBufferToString(data), socketId);
return originalSendBinary(data, socketId);
};

nativeWebSocketModule.close = (...args) => {
if (closeCallback) {
if (args.length === 3) {
closeCallback(args[0], args[1], args[2]);
} else {
closeCallback(null, null, args[0]);
}
}
return originalClose(...args);
};

isInterceptorEnabled = true;
},
disableInterception() {
if (!isInterceptorEnabled) {
return;
}

isInterceptorEnabled = false;

nativeWebSocketModule.connect = originalConnect;
nativeWebSocketModule.send = originalSend;
nativeWebSocketModule.sendBinary = originalSendBinary;
nativeWebSocketModule.close = originalClose;

connectCallback = null;
closeCallback = null;
sendCallback = null;
onOpenCallback = null;
onMessageCallback = null;
onCloseCallback = null;
onErrorCallback = null;

unregisterEvents();
},
};
};
Loading
Loading