From 5f2d214a9b4c640e8761133a354043a5d0bb176b Mon Sep 17 00:00:00 2001 From: Tom Swindell Date: Sun, 17 May 2026 16:38:09 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20js-libp2p=20compatibility=20=E2=80=94?= =?UTF-8?q?=20zero-copy=20JSI=20data=20path=20+=20Node=20net.Socket=20half?= =?UTF-8?q?-open=20parity=20(#209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #209 (Task 2: RN clients failing to connect to libp2p bootstrap/relay/peer nodes with `UnexpectedEOFError`). Task 1 (#183, `net.createServer(options, cb)`) is already resolved as of v6.4.1 — no code change needed. Root-caused four independent defects that made js-libp2p v3 unusable on Android RN 0.83 (New Arch / bridgeless / Hermes), each device-proven on a physical device (Nokia 8.3 5G, arm64-v8a) against the public Amino DHT swarm: 1. connect() option-graph OOM. @libp2p/tcp spreads its entire cyclic dial-options object (signal/upgrader/components) into the net.connect() arg; RN's jsi::dynamicFromValue recursively explodes it into millions of folly::dynamic nodes → Scudo OOM in seconds. Fixed with an explicit scalar allow-list (the native side only ever reads those keys). 2. Per-chunk legacy-bridge data path OOM. Any per-chunk crossing of the RCTDeviceEventEmitter / invokeJavaMethod bridge backlogs unbounded folly::dynamic under libp2p volume. Replaced inbound + outbound byte movement AND the readable/written signals with a zero-copy JSI HostObject (cpp/TcpDataBridge) driven by the C++ CallInvoker — bytes and signals never transit folly::dynamic. 3. JNI registration lifetime. In the New-Arch single-merged-.so model the data-bridge JNI_OnLoad ran pre-bundle under the bootstrap classloader; native methods failed to bind. Fixed with an implicitly-bound, Java-driven install entry + a link-time keep anchor (defeats --gc-sections stripping of the runtime-only TU). 4. JSI install guard on a HostObject. The "callback installed" guard was stashed as a property on the JSI HostObject; Hermes rejects arbitrary-property writes on a HostObject with a default setter (TypeError), which was swallowed → bridge null → every noise write failed → every dial silently abandoned (conns=0). Moved the guards to module-level state. Also implements the Node net.Socket half-open / teardown contract @libp2p/tcp depends on (and which the bounty's connect failure surfaces): - connect() now reads `options.allowHalfOpen` (kept off the native bridge — JS lifecycle flag only). - Adds socket.destroySoon() (libp2p sendClose calls it on every graceful close — its absence threw `is not a function` on every connection teardown). - Adds socket.resetAndDestroy() (libp2p sendReset). - 'close' now emits a boolean `hadError` per the Node spec (was the raw error object, working only by truthiness). - destroy(error) accepts an optional error per the Node signature. iOS JSI parity is a scoped follow-up: the shared JS changes are iOS-correct, but the C++ data path is not yet wired into the iOS pod. The legacy iOS path is unaffected; only the new zero-copy JSI path is Android-only for now. Adds __tests__/halfOpen209.test.js (6 tests). Full suite green (16/16). No version bump (release is the maintainer's call). Server.js / index.js / TLS*.js intentionally untouched. --- __tests__/halfOpen209.test.js | 146 ++++++ android/CMakeLists.txt | 34 ++ android/TcpBridgeJni.cpp | 189 ++++++++ .../react/tcpsocket/TcpDataBridgeNative.java | 110 +++++ .../react/tcpsocket/TcpEventListener.java | 36 +- .../react/tcpsocket/TcpSocketClient.java | 166 ++++++- .../react/tcpsocket/TcpSocketModule.java | 17 +- coverage/coverage-final.json | 7 - cpp/TcpDataBridge.cpp | 438 ++++++++++++++++++ cpp/TcpDataBridge.h | 76 +++ cpp/TcpInboundRegistry.cpp | 184 ++++++++ cpp/TcpInboundRegistry.h | 146 ++++++ package.json | 13 + react-native.config.js | 23 + src/Globals.js | 162 ++++++- src/NativeTcpDataBridge.ts | 37 ++ src/Socket.js | 328 ++++++++++--- 17 files changed, 1979 insertions(+), 133 deletions(-) create mode 100644 __tests__/halfOpen209.test.js create mode 100644 android/CMakeLists.txt create mode 100644 android/TcpBridgeJni.cpp create mode 100644 android/src/main/java/com/asterinet/react/tcpsocket/TcpDataBridgeNative.java delete mode 100644 coverage/coverage-final.json create mode 100644 cpp/TcpDataBridge.cpp create mode 100644 cpp/TcpDataBridge.h create mode 100644 cpp/TcpInboundRegistry.cpp create mode 100644 cpp/TcpInboundRegistry.h create mode 100644 react-native.config.js create mode 100644 src/NativeTcpDataBridge.ts diff --git a/__tests__/halfOpen209.test.js b/__tests__/halfOpen209.test.js new file mode 100644 index 0000000..e32ebdc --- /dev/null +++ b/__tests__/halfOpen209.test.js @@ -0,0 +1,146 @@ +import { expect, test, jest, beforeEach } from '@jest/globals'; +import net from '../src/index'; +import { nativeEventEmitter } from '../src/Globals'; +import { NativeModules } from 'react-native'; + +const Sockets = NativeModules.TcpSockets; + +// Mirror the Globals mock used by allowHalfOpen.test.js so socket ids +// and the native event emitter behave deterministically. +jest.mock('../src/Globals', () => { + const { EventEmitter } = require('events'); + const emitter = new EventEmitter(); + const originalAddListener = emitter.addListener.bind(emitter); + // @ts-ignore + emitter.addListener = (event, listener) => { + originalAddListener(event, listener); + return { remove: () => emitter.removeListener(event, listener) }; + }; + let idCounter = 2000; + return { + __esModule: true, + nativeEventEmitter: emitter, + getNextId: () => idCounter++, + // Socket.js guards every JSI call with `typeof fn === 'function'`, + // so omitting these here exercises the no-bridge degrade path. + }; +}); + +beforeEach(() => { + Sockets.connect.mockClear(); + Sockets.end.mockClear(); + Sockets.destroy.mockClear(); + Sockets.write.mockClear(); +}); + +/** + * #209 / #183 Node `net.Socket` parity. `@libp2p/tcp` requires these + * exact APIs: it passes `allowHalfOpen` through `net.connect(cOpts)`, + * calls `socket.destroySoon()` on every graceful close, calls + * `socket.resetAndDestroy()` for resets, and reads a BOOLEAN `hadError` + * from the `'close'` event. + */ + +test('connect() reads allowHalfOpen from the options object (libp2p passes it through net.connect)', () => { + const socket = new net.Socket(); + expect(socket.allowHalfOpen).toBe(false); + socket.connect({ port: 1234, host: '127.0.0.1', allowHalfOpen: true }); + expect(socket.allowHalfOpen).toBe(true); + // It must NOT be forwarded to the native connect args (JS lifecycle + // flag only — and arbitrary caller objects must never cross). + const customOptions = Sockets.connect.mock.calls[0][3]; + expect(customOptions.allowHalfOpen).toBeUndefined(); +}); + +test('destroySoon() exists and, when nothing is buffered after end(), destroys immediately', () => { + const socket = new net.Socket(); + socket.connect({ port: 1, host: 'h' }); + // simulate native connect ack + socket._setConnected({ + localAddress: '127.0.0.1', + localPort: 1, + remoteAddress: 'h', + remotePort: 1, + remoteFamily: 'IPv4', + }); + expect(typeof socket.destroySoon).toBe('function'); + socket.end(); // writable finished, no buffered bytes + socket.destroySoon(); + expect(Sockets.destroy).toHaveBeenCalled(); +}); + +test('destroySoon() with unflushed write defers destroy until close', () => { + const socket = new net.Socket(); + socket.connect({ port: 1, host: 'h' }); + socket._setConnected({ + localAddress: '127.0.0.1', + localPort: 1, + remoteAddress: 'h', + remotePort: 1, + remoteFamily: 'IPv4', + }); + // Simulate unflushed outbound bytes. (Going through write() would hit + // the intentional loud throw — the Jest env has no JSI bridge — so + // set the buffered-size accumulator directly: destroySoon's decision + // is exactly `_writableEnded && _writeBufferSize === 0`.) + socket._writeBufferSize = 13; + socket.destroySoon(); + // FIN sent, but destroy deferred until 'close' + expect(Sockets.end).toHaveBeenCalled(); + expect(Sockets.destroy).not.toHaveBeenCalled(); + // native reports the socket closed → now it tears down + nativeEventEmitter.emit('close', { id: socket._id }); + expect(Sockets.destroy).toHaveBeenCalled(); +}); + +test('resetAndDestroy() exists and destroys the socket', () => { + const socket = new net.Socket(); + socket.connect({ port: 1, host: 'h' }); + socket._setConnected({ + localAddress: '127.0.0.1', + localPort: 1, + remoteAddress: 'h', + remotePort: 1, + remoteFamily: 'IPv4', + }); + expect(typeof socket.resetAndDestroy).toBe('function'); + socket.resetAndDestroy(); + expect(Sockets.destroy).toHaveBeenCalled(); + expect(socket.destroyed).toBe(true); +}); + +test("'close' emits a BOOLEAN hadError (Node net.Socket spec), not the error object", () => { + // A socket emits 'close' exactly once and detaches its native + // listeners on disconnect, so each case needs its own socket. + const clean = new net.Socket(); + clean.connect({ port: 1, host: 'h' }); + let cleanArg; + clean.on('close', (hadError) => { + cleanArg = hadError; + }); + nativeEventEmitter.emit('close', { id: clean._id }); + + const errored = new net.Socket(); + errored.connect({ port: 2, host: 'h' }); + let erroredArg; + errored.on('close', (hadError) => { + erroredArg = hadError; + }); + nativeEventEmitter.emit('close', { id: errored._id, error: new Error('boom') }); + + expect(cleanArg).toBe(false); + expect(erroredArg).toBe(true); + expect(typeof cleanArg).toBe('boolean'); + expect(typeof erroredArg).toBe('boolean'); +}); + +test('destroy(error) emits the error then marks destroyed', () => { + const socket = new net.Socket(); + socket.connect({ port: 1, host: 'h' }); + const errs = []; + socket.on('error', (e) => errs.push(e)); + const boom = new Error('reset'); + socket.destroy(boom); + expect(errs).toEqual([boom]); + expect(socket.destroyed).toBe(true); +}); diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 0000000..ac1db85 --- /dev/null +++ b/android/CMakeLists.txt @@ -0,0 +1,34 @@ +# VENHO fork — Phase 1 zero-copy data-plane native build. +# Modeled on react-native-quick-base64/android/CMakeLists.txt (proven +# RN-0.83 New-Arch cxxTurboModule wiring in this same app). +cmake_minimum_required(VERSION 3.13.0) +project(TcpDataBridge) + +set(PACKAGE_NAME "react-native-tcp-socket") +set(CMAKE_VERBOSE_MAKEFILE ON) + +add_library( + ${PACKAGE_NAME} STATIC + ../cpp/TcpDataBridge.cpp + ../cpp/TcpInboundRegistry.cpp + TcpBridgeJni.cpp +) + +set_target_properties( + ${PACKAGE_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +target_include_directories( + ${PACKAGE_NAME} PUBLIC + ../cpp +) + +target_link_libraries( + ${PACKAGE_NAME} jsi + fbjni + reactnative + react_codegen_RNTcpSocketsSpec +) diff --git a/android/TcpBridgeJni.cpp b/android/TcpBridgeJni.cpp new file mode 100644 index 0000000..dc159e7 --- /dev/null +++ b/android/TcpBridgeJni.cpp @@ -0,0 +1,189 @@ +// VENHO fork — Phase 1 JNI glue: Java socket read thread → C++ inbound +// registry. The Java TcpReceiverTask, instead of base64+WritableMap+ +// RCTDeviceEventEmitter (the Phase-0 folly::dynamic OOM path), calls +// these to push raw bytes into the bounded C++ queue the JSI host +// object drains zero-copy. +// +// Registered via explicit JNI (RegisterNatives in JNI_OnLoad) for the +// Java class com.asterinet.react.tcpsocket.TcpDataBridgeNative. + +#include "TcpDataBridge.h" +#include "TcpInboundRegistry.h" + +#include + +#include +#include + +namespace { + +// pushInbound(int id, byte[] data, int len) -> boolean +// Returns false when the bounded queue hit the high watermark and the +// Java read loop must pause (registry fires resume via the no-op below; +// Java polls canResume()). +jboolean nativePushInbound(JNIEnv* env, jclass, jint id, jbyteArray data, + jint len) { + jbyte* buf = env->GetByteArrayElements(data, nullptr); + bool keepReading = venho::TcpInboundRegistry::instance().pushInbound( + static_cast(id), reinterpret_cast(buf), + static_cast(len)); + env->ReleaseByteArrayElements(data, buf, JNI_ABORT); // no copy-back + return keepReading ? JNI_TRUE : JNI_FALSE; +} + +// registerSocket(int id): create the bounded queue. The "resume" signal +// is polled by Java (canResume) rather than a JNI upcall, to keep this +// glue minimal and avoid attaching the C++ caller thread to the JVM. +void nativeRegisterSocket(JNIEnv*, jclass, jint id) { + // Java polls canResume() for backpressure — no resume callback. + venho::TcpInboundRegistry::instance().registerSocket( + static_cast(id)); +} + +void nativeUnregisterSocket(JNIEnv*, jclass, jint id) { + venho::TcpInboundRegistry::instance().unregisterSocket( + static_cast(id)); + // Drop this socket's readable-coalescing pending entry so the + // process-global map stays bounded across many short-lived libp2p + // peer connections. + facebook::react::TcpDataBridge::forgetSocket(static_cast(id)); +} + +// canResume(int id): true once the queue drained below the low +// watermark (Java's paused read loop polls this on a short wait). +jboolean nativeCanResume(JNIEnv*, jclass, jint id) { + return venho::TcpInboundRegistry::instance().canResume( + static_cast(id)) + ? JNI_TRUE + : JNI_FALSE; +} + +// waitWriteChunk(int id, int[] msgIdOut) -> byte[] | null +// Blocks the native write thread until the socket's OUTBOUND queue (fed +// zero-copy by the JSI write() host fn) has a chunk, then returns its +// bytes and writes the chunk's msgId into msgIdOut[0]. Returns null when +// the socket was unregistered (write thread must exit). One JNI call; +// the only allocation is the unavoidable jbyteArray that must cross to +// Java — NO base64, NO folly::dynamic, NO @ReactMethod. +jbyteArray nativeWaitWriteChunk(JNIEnv* env, jclass, jint id, + jintArray msgIdOut) { + venho::OutboundChunk chunk; + if (!venho::TcpInboundRegistry::instance().waitPopOutbound( + static_cast(id), chunk)) { + return nullptr; // socket gone — signal the write thread to exit + } + const auto len = static_cast(chunk.bytes.size()); + jbyteArray out = env->NewByteArray(len); + if (out == nullptr) { + return nullptr; // OOM on the Java heap — let the write thread exit + } + if (len > 0) { + env->SetByteArrayRegion( + out, 0, len, reinterpret_cast(chunk.bytes.data())); + } + if (msgIdOut != nullptr && env->GetArrayLength(msgIdOut) >= 1) { + jint mid = static_cast(chunk.msgId); + env->SetIntArrayRegion(msgIdOut, 0, 1, &mid); + } + return out; +} + +// signalReadable(int id): "data available for socket id" — hops to the +// JS thread via the CallInvoker and calls the registered JS readable +// callback. This is the legacy-bridge-FREE replacement for the old +// RCTDeviceEventEmitter "readable" emit that still OOM'd in 1d. +void nativeSignalReadable(JNIEnv*, jclass, jint id) { + facebook::react::TcpDataBridge::signalReadable(static_cast(id)); +} + +// signalWritten(int id, int msgId, String err): per-write ACK — hops to +// the JS thread via the CallInvoker and calls the registered JS +// `written` callback with (id,msgId,err). err is "" on success (Java +// passes null → treated as ""). Legacy-bridge-FREE replacement for the +// RCTDeviceEventEmitter "written" emit that accumulated an unbounded +// folly::dynamic per write (Scenario-C OOM #2). The std::string is +// copied before the async hop, so the Java string may be released. +void nativeSignalWritten(JNIEnv* env, jclass, jint id, jint msgId, + jstring err) { + std::string errStr; + if (err != nullptr) { + const char* c = env->GetStringUTFChars(err, nullptr); + if (c != nullptr) { + errStr.assign(c); + env->ReleaseStringUTFChars(err, c); + } + } + facebook::react::TcpDataBridge::signalWritten( + static_cast(id), static_cast(msgId), errStr); +} + +const JNINativeMethod kMethods[] = { + {"nativePushInbound", "(I[BI)Z", + reinterpret_cast(nativePushInbound)}, + {"nativeRegisterSocket", "(I)V", + reinterpret_cast(nativeRegisterSocket)}, + {"nativeUnregisterSocket", "(I)V", + reinterpret_cast(nativeUnregisterSocket)}, + {"nativeCanResume", "(I)Z", + reinterpret_cast(nativeCanResume)}, + {"nativeSignalReadable", "(I)V", + reinterpret_cast(nativeSignalReadable)}, + {"nativeSignalWritten", "(IILjava/lang/String;)V", + reinterpret_cast(nativeSignalWritten)}, + {"nativeWaitWriteChunk", "(I[I)[B", + reinterpret_cast(nativeWaitWriteChunk)}, +}; + +} // namespace + +// VENHO Phase 1 — JNI registration LIFETIME fix (replaces JNI_OnLoad). +// +// The fork's native code is autolinked as a STATIC lib and merged into +// libappmodules.so (RN New-Arch single-merged-lib model: see +// android/CMakeLists.txt + the generated Android-autolinking.cmake). +// libappmodules.so has exactly ONE JNI_OnLoad, invoked by SoLoader VERY +// early — before the JS bundle runs, with the bootstrap classloader. +// FindClass("com/asterinet/.../TcpDataBridgeNative") from THAT context +// returns null (the RN app classes aren't reachable from the bootstrap +// loader), so the old JNI_OnLoad returned JNI_ERR and the 7 natives were +// NEVER registered → UnsatisfiedLinkError when libp2p first tore a +// socket down (nativeUnregisterSocket). Per-`.so` JNI_OnLoad is NOT +// re-invoked, so ensureLoaded()'s loadLibrary couldn't recover it. +// +// Fix: ONE *implicitly* bound bootstrap method, +// Java_com_asterinet_react_tcpsocket_TcpDataBridgeNative_nativeInstallBridge. +// Implicit JNI binding resolves Java__ by exported-symbol +// lookup across already-loaded .so's — no JNI_OnLoad, no RegisterNatives +// needed for IT. Java's TcpDataBridgeNative.ensureLoaded() calls it once; +// because the call originates in Java, `env` carries the APP classloader +// and `clazz` IS TcpDataBridgeNative — so the explicit RegisterNatives +// for the other 7 succeeds deterministically, at first socket use. +extern "C" JNIEXPORT void JNICALL +Java_com_asterinet_react_tcpsocket_TcpDataBridgeNative_nativeInstallBridge( + JNIEnv* env, jclass clazz) { + // `clazz` is TcpDataBridgeNative itself (passed by the JVM for a + // static native). No FindClass / classloader hazard. + if (env->RegisterNatives(clazz, kMethods, + sizeof(kMethods) / sizeof(kMethods[0])) != 0) { + // Leave any pending exception for Java to surface (ensureLoaded's + // caller will see it) rather than silently swallowing — a hard, + // loud failure here is correct: the data plane cannot work without + // these natives, and a quiet failure would resurface later as the + // same opaque UnsatisfiedLinkError this fix eliminates. + } +} + +// VENHO Phase 1 — link-time KEEP anchor. +// +// This TU only defines a JNI entry point that is resolved at RUNTIME by +// name (implicit binding). Nothing references it at LINK time, so the +// merged-lib link (NDK default --gc-sections) dead-strips this entire +// object file out of libappmodules.so — exactly what happened on the +// first attempt (the .a had the symbol; the merged .so did not, so +// implicit JNI lookup found nothing). TcpDataBridge.cpp IS kept (the +// codegen cxxTurboModule references it), so having its constructor touch +// this no-op creates the single link-time edge that pulls TcpBridgeJni +// .cpp.o — and therefore the nativeInstallBridge export — into the .so. +// Must be a real, externally-visible definition (not inline / not +// static) so the reference cannot be optimised away. +extern "C" void venho_tcpBridgeJniKeepAnchor() {} diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpDataBridgeNative.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpDataBridgeNative.java new file mode 100644 index 0000000..8639173 --- /dev/null +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpDataBridgeNative.java @@ -0,0 +1,110 @@ +package com.asterinet.react.tcpsocket; + +/** + * VENHO fork — Phase 1 zero-copy socket data bridge (Java side), BOTH + * directions. + * + * INBOUND: the native socket read thread pushes received bytes straight + * into the C++ {@code TcpInboundRegistry} via these JNI calls, instead + * of the legacy {@code Base64.encodeToString} → WritableMap → + * RCTDeviceEventEmitter path that built a folly::dynamic per chunk and + * OOM'd the process under libp2p volume (Phase 0). + * + * OUTBOUND: the native socket write thread pulls bytes to send from the + * same C++ registry ({@link #nativeWaitWriteChunk}) — the JSI host + * {@code write()} enqueued them zero-copy off the JS ArrayBuffer. This + * replaces the {@code @ReactMethod write(int, String base64, int)} Java + * TurboModule call whose per-write jsi::dynamicFromValue marshalling was + * the residual Scudo OOM (Scenario C, after inbound was fixed). + * + * Backpressure: {@link #nativePushInbound} returns false when the + * bounded C++ queue hit its high watermark — the read loop must then + * stop reading the socket (TCP flow-control backpressures the peer) and + * poll {@link #nativeCanResume} until the JS side has drained it. The + * outbound queue is bounded symmetrically; the JSI {@code write()} + * returns false at its high watermark (JS latches writableNeedDrain). + * + * The JNI methods are registered LAZILY from C++ via the implicitly + * bound {@link #nativeInstallBridge()} (TcpBridgeJni.cpp), called once + * from {@link #ensureLoaded()}. NOT from JNI_OnLoad: the fork's native + * code is merged into libappmodules.so, whose single JNI_OnLoad runs + * pre-bundle with the bootstrap classloader, where FindClass for this + * class returns null. There is no standalone libreact-native-tcp-socket + * .so to load — the symbols already live in the loaded libappmodules.so. + */ +final class TcpDataBridgeNative { + + private static boolean bridgeInstalled = false; + + private TcpDataBridgeNative() {} + + static synchronized void ensureLoaded() { + if (!bridgeInstalled) { + // Implicitly bound (Java__nativeInstallBridge) — the + // JVM resolves it by symbol lookup across already-loaded + // .so's, so no System.loadLibrary is needed. It RegisterNatives + // the 7 data-plane methods using THIS call's app-classloader + // env, which is why it must run from here (not JNI_OnLoad). + nativeInstallBridge(); + bridgeInstalled = true; + } + } + + /** + * One-time bootstrap: explicitly registers the 7 data-plane native + * methods below. Implicitly bound (no JNI_OnLoad / no loadLibrary). + * Must be invoked from Java so the C++ side gets the app classloader + * and the {@code TcpDataBridgeNative} jclass deterministically. + */ + private static native void nativeInstallBridge(); + + static native void nativeRegisterSocket(int id); + + static native void nativeUnregisterSocket(int id); + + /** @return true to keep reading; false → high watermark, pause. */ + static native boolean nativePushInbound(int id, byte[] data, int len); + + /** @return true once drained to low watermark (safe to resume). */ + static native boolean nativeCanResume(int id); + + /** + * Block until the socket's OUTBOUND queue (filled zero-copy by the + * JSI {@code write()} host fn) has a chunk, then return its bytes. + * The chunk's {@code msgId} is written to {@code msgIdOut[0]} (a + * reusable single-element array — one JNI call, no TOCTOU, no + * folly::dynamic). Returns {@code null} when the socket was + * unregistered: the write thread must then exit. + * + * @param id socket id + * @param msgIdOut int[1] out-param receiving the chunk's msgId + * @return the bytes to write, or null to signal thread exit + */ + static native byte[] nativeWaitWriteChunk(int id, int[] msgIdOut); + + /** + * "Data available for socket id" — hops to the JS thread via the + * CallInvoker and invokes the registered JS readable callback. The + * legacy-bridge-FREE replacement for the old RCTDeviceEventEmitter + * "readable" emit (which still OOM'd: per-chunk invokeJavaMethod → + * folly::dynamic backlog, independent of payload size). + */ + static native void nativeSignalReadable(int id); + + /** + * Per-write ACK — hops to the JS thread via the CallInvoker and + * invokes the registered JS {@code written} callback with + * {@code (id, msgId, err)}. {@code err} is "" on success (pass + * {@code null} for success — the native side maps it to ""). The + * legacy-bridge-FREE replacement for the {@code onWritten} → + * {@code RCTDeviceEventEmitter.emit("written",…)} path, which ran + * once PER WRITE and accumulated an unbounded folly::dynamic in the + * bridgeless event-emitter queue under libp2p volume (Scenario-C + * OOM #2 — same class of bug milestone-1d hit on readable). + * + * @param id socket id + * @param msgId the write's msgId (echoed to the JS write callback) + * @param err error message, or null for success + */ + static native void nativeSignalWritten(int id, int msgId, String err); +} diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java index 7c1c6ee..9099791 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java @@ -14,8 +14,6 @@ import java.net.ServerSocket; import java.net.Socket; -import javax.annotation.Nullable; - public class TcpEventListener { private final DeviceEventManagerModule.RCTDeviceEventEmitter rctEvtEmitter; @@ -84,13 +82,12 @@ public void onListen(int id, TcpSocketServer server) { sendEvent("listening", eventParams); } - public void onData(int id, byte[] data) { - WritableMap eventParams = Arguments.createMap(); - eventParams.putInt("id", id); - eventParams.putString("data", Base64.encodeToString(data, Base64.NO_WRAP)); - - sendEvent("data", eventParams); - } + // VENHO Phase 1: the read loop no longer calls onData OR a legacy + // "readable" emit. Bytes go into the C++ TcpInboundRegistry via JNI; + // the readable signal is delivered through the JSI CallInvoker + // (TcpDataBridge::signalReadable), NOT this RCTDeviceEventEmitter. + // Milestone-1d proved ANY per-chunk legacy-bridge crossing OOMs + // (invokeJavaMethod → folly::dynamic backlog), even a tiny {id} map. public void onEnd(int id) { WritableMap eventParams = Arguments.createMap(); @@ -98,19 +95,14 @@ public void onEnd(int id) { sendEvent("end", eventParams); } - public void onWritten(int id, int msgId, @Nullable Exception e) { - String error = null; - if (e != null) { - Log.e(TcpSocketModule.TAG, "Exception on socket " + id, e); - error = e.getMessage(); - } - WritableMap eventParams = Arguments.createMap(); - eventParams.putInt("id", id); - eventParams.putInt("msgId", msgId); - eventParams.putString("err", error); - - sendEvent("written", eventParams); - } + // VENHO Phase 1: onWritten() is REMOVED. It ran once PER WRITE and + // built a WritableMap → RCTDeviceEventEmitter.emit("written",…), + // accumulating an unbounded nested folly::dynamic in the bridgeless + // event-emitter queue under libp2p volume — Scenario-C OOM #2 (same + // class of bug milestone-1d hit on the readable side). The per-write + // ACK now goes through the JSI CallInvoker + // (TcpDataBridge::signalWritten via TcpSenderTask), never this + // legacy bridge. pre-Alpha = clean break, not a stub. public void onClose(int id, Exception e) { if (e != null) { diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java index e0f719f..0fa69ac 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java @@ -21,11 +21,27 @@ class TcpSocketClient extends TcpSocket { private final ExecutorService listenExecutor; + // VENHO Phase 1: outbound is no longer a per-call @ReactMethod + // Runnable (that base64 String arg crossed the Java TurboModule's + // jsi::dynamicFromValue — the residual Scenario-C OOM). A dedicated + // write LOOP thread blocks on the C++ outbound queue (fed zero-copy + // by the JSI write() host fn) and writes to the socket — symmetric + // to the inbound TcpReceiverTask. private final ExecutorService writeExecutor; private final TcpEventListener receiverListener; private TcpReceiverTask receiverTask; + private TcpSenderTask senderTask; private Socket socket; private boolean closed = true; + // VENHO Phase 1: true once this socket's C++ queues were registered + // (startListening → nativeRegisterSocket). destroy()'s finally must + // NOT call nativeUnregisterSocket otherwise: a connect() that throws + // before startListening (e.g. an unreachable peer) never registered, + // and an unconditional unregister would (a) double-handle an unknown + // id and (b) — until JNI is first installed via ensureLoaded — throw + // UnsatisfiedLinkError from the cleanup path, masking the real + // connect failure. Registration always precedes any teardown. + private volatile boolean nativeRegistered = false; TcpSocketClient(TcpEventListener receiverListener, Integer id, Socket socket) { super(id); @@ -142,32 +158,98 @@ private SSLSocketFactory getSSLSocketFactory(Context context, ReadableMap tlsOpt } public void startListening() { + // VENHO Phase 1: register this socket's bounded inbound+outbound + // queues in the C++ registry before the loops start using them, + // then start BOTH the read loop and the dedicated write loop. + TcpDataBridgeNative.ensureLoaded(); + TcpDataBridgeNative.nativeRegisterSocket(getId()); + nativeRegistered = true; receiverTask = new TcpReceiverTask(this, receiverListener); listenExecutor.execute(receiverTask); + senderTask = new TcpSenderTask(this, receiverListener); + writeExecutor.execute(senderTask); } + // VENHO Phase 1: the old per-call write(int, byte[]) Runnable (driven + // by the @ReactMethod write(int, String base64, int) Java + // TurboModule call) is GONE — that base64 String arg crossed + // jsi::dynamicFromValue per write and was the residual Scenario-C + // Scudo OOM. Outbound now flows: JS Buffer → JSI write() host fn + // (one copy off the ArrayBuffer into the bounded C++ queue, no + // base64/folly) → TcpSenderTask drains it here and writes to the + // socket. The `written` ack still goes through receiverListener (one + // tiny {id,msgId,err} map, NOT per-payload — acceptable; only the + // bulk data path was the leak). + /** - * Sends data from the socket - * - * @param data data to be sent + * VENHO Phase 1 outbound write loop. Blocks on the C++ outbound + * queue (fed zero-copy by the JSI write() host fn), writes each + * chunk to the socket OutputStream, and ACKs each write via the JSI + * CallInvoker ({@code nativeSignalWritten}) — NOT the legacy + * {@code receiverListener.onWritten} → RCTDeviceEventEmitter path, + * which ran once PER WRITE and accumulated an unbounded + * folly::dynamic in the bridgeless event-emitter queue under libp2p + * volume (Scenario-C OOM #2). Terminal {@code onError} stays on the + * legacy bridge: it fires AT MOST ONCE per socket lifetime (every + * error path here returns immediately), so it is not a per-chunk + * crossing — same rationale as close/end/connect. Exits when + * nativeWaitWriteChunk returns null (socket unregistered in + * destroy()). Symmetric to TcpReceiverTask. */ - public void write(final int msgId, final byte[] data) { - writeExecutor.execute(new Runnable() { - @Override - public void run() { - if (socket == null) { - receiverListener.onError(getId(), new IOException("Attempted to write to closed socket")); - return; + private static class TcpSenderTask implements Runnable { + private final TcpSocketClient clientSocket; + private final TcpEventListener receiverListener; + + TcpSenderTask(TcpSocketClient clientSocket, TcpEventListener receiverListener) { + this.clientSocket = clientSocket; + this.receiverListener = receiverListener; + } + + @Override + public void run() { + final int socketId = clientSocket.getId(); + // Reused single-element out-param for the chunk's msgId — one + // JNI call per chunk, no per-write allocation beyond the + // unavoidable payload byte[]. + final int[] msgIdOut = new int[1]; + try { + while (true) { + byte[] data = TcpDataBridgeNative.nativeWaitWriteChunk(socketId, msgIdOut); + if (data == null) { + // Socket unregistered (destroy()) — exit the loop. + return; + } + final int msgId = msgIdOut[0]; + Socket socket = clientSocket.getSocket(); + if (socket == null) { + // ACK the failure via JSI (per-write path), then + // the terminal onError once (legacy, one-shot). + TcpDataBridgeNative.nativeSignalWritten(socketId, msgId, + "Attempted to write to closed socket"); + receiverListener.onError(socketId, + new IOException("Attempted to write to closed socket")); + return; + } + try { + socket.getOutputStream().write(data); + // Per-write ACK over the JSI CallInvoker (null + // err → "" success). NOT onWritten/legacy bridge. + TcpDataBridgeNative.nativeSignalWritten(socketId, msgId, null); + } catch (IOException e) { + TcpDataBridgeNative.nativeSignalWritten(socketId, msgId, + e.getMessage() != null ? e.getMessage() : "write failed"); + if (!clientSocket.closed) { + receiverListener.onError(socketId, e); + } + return; + } } - try { - socket.getOutputStream().write(data); - receiverListener.onWritten(getId(), msgId, null); - } catch (IOException e) { - receiverListener.onWritten(getId(), msgId, e); - receiverListener.onError(getId(), e); + } catch (Exception e) { + if (!clientSocket.closed) { + receiverListener.onError(socketId, e); } } - }); + } } public ReadableMap getPeerCertificate() { @@ -192,6 +274,17 @@ public void destroy() { } } catch (IOException e) { receiverListener.onClose(getId(), e); + } finally { + // VENHO Phase 1: free this socket's C++ inbound queue — but + // ONLY if startListening actually registered it. A connect() + // that threw before startListening (unreachable peer) never + // registered, so unregistering here would be a no-op at best + // and, before the first ensureLoaded, an UnsatisfiedLinkError + // that masks the real connect failure. + if (nativeRegistered) { + nativeRegistered = false; + TcpDataBridgeNative.nativeUnregisterSocket(getId()); + } } } @@ -262,10 +355,33 @@ public void run() { try { BufferedInputStream in = new BufferedInputStream(socket.getInputStream()); while (!socket.isClosed()) { - int bufferCount = in.read(buffer); + // VENHO Phase 1: throttle BEFORE the read (was after). + // If the C++ queue is at the high watermark, do not + // read more from the socket — TCP flow control then + // backpressures the peer (the structural fix for the + // Phase-0 unbounded folly::dynamic backlog). Honour an + // explicit JS pause() too. waitIfPaused(); + waitForNativeDrain(socketId); + int bufferCount = in.read(buffer); if (bufferCount > 0) { - receiverListener.onData(socketId, Arrays.copyOfRange(buffer, 0, bufferCount)); + // Zero legacy bridge: push raw bytes into the C++ + // registry (no base64, no WritableMap, no + // RCTDeviceEventEmitter). Return false => high + // watermark; loop back and waitForNativeDrain + // gates the next read. + TcpDataBridgeNative.nativePushInbound( + socketId, + Arrays.copyOfRange(buffer, 0, bufferCount), + bufferCount); + // Readable signal via the JSI CallInvoker (NOT + // the legacy RCTDeviceEventEmitter — that still + // OOM'd in 1d: per-chunk invokeJavaMethod → + // folly::dynamic backlog regardless of payload). + // C++ hops to the JS thread and calls the JS + // readable cb with just the id; bytes are pulled + // zero-copy via the JSI read(id). + TcpDataBridgeNative.nativeSignalReadable(socketId); } else if (bufferCount == -1) { receiverListener.onEnd(socketId); break; @@ -278,6 +394,18 @@ public void run() { } } + /** + * Block the read loop while the C++ inbound queue is over its + * high watermark, polling until JS has drained it below the low + * watermark. Short sleep poll (vs a JNI upcall) keeps the native + * glue minimal; the peer's TCP window stalls meanwhile. + */ + private void waitForNativeDrain(int socketId) throws InterruptedException { + while (!TcpDataBridgeNative.nativeCanResume(socketId)) { + Thread.sleep(4); + } + } + public synchronized void pause() { paused = true; } diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java index 121edb4..1f8a359 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java @@ -9,7 +9,6 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; -import android.util.Base64; import android.util.Log; import androidx.annotation.NonNull; @@ -124,14 +123,14 @@ public void startTLS(final int cId, @NonNull final ReadableMap tlsOptions) { } } - @SuppressLint("StaticFieldLeak") - @SuppressWarnings("unused") - @ReactMethod - public void write(final int cId, @NonNull final String base64String, final int msgId) { - TcpSocketClient socketClient = getTcpClient(cId); - byte[] data = Base64.decode(base64String, Base64.NO_WRAP); - socketClient.write(msgId, data); - } + // VENHO Phase 1: the @ReactMethod write(int, String base64, int) is + // REMOVED. Its base64 String arg crossed the Java TurboModule's + // jsi::dynamicFromValue per write — the residual Scenario-C Scudo + // OOM after inbound was fixed. Outbound now flows entirely through + // the JSI data plane: Socket.js → global.__TcpDataBridge.write(id, + // ArrayBuffer, msgId) → bounded C++ outbound queue → TcpSenderTask + // (one copy off the JS heap, no base64, no folly::dynamic). Nothing + // calls this method anymore; pre-Alpha = clean break, not a stub. @SuppressLint("StaticFieldLeak") @SuppressWarnings("unused") diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json deleted file mode 100644 index 7486194..0000000 --- a/coverage/coverage-final.json +++ /dev/null @@ -1,7 +0,0 @@ -{"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Globals.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Globals.js","statementMap":{"0":{"start":{"line":2,"column":16},"end":{"line":2,"column":40}},"1":{"start":{"line":4,"column":21},"end":{"line":4,"column":22}},"2":{"start":{"line":7,"column":4},"end":{"line":7,"column":28}},"3":{"start":{"line":10,"column":27},"end":{"line":10,"column":58}}},"fnMap":{"0":{"name":"getNextId","decl":{"start":{"line":6,"column":9},"end":{"line":6,"column":18}},"loc":{"start":{"line":6,"column":21},"end":{"line":8,"column":1}},"line":6}},"branchMap":{},"s":{"0":1,"1":1,"2":4,"3":1},"f":{"0":4},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"8cb15bee01bf57f8ac938fbc20fc9feb7945c83c"} -,"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Server.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Server.js","statementMap":{"0":{"start":{"line":9,"column":16},"end":{"line":9,"column":40}},"1":{"start":{"line":36,"column":8},"end":{"line":36,"column":16}},"2":{"start":{"line":38,"column":8},"end":{"line":38,"column":31}},"3":{"start":{"line":40,"column":8},"end":{"line":40,"column":48}},"4":{"start":{"line":43,"column":8},"end":{"line":43,"column":38}},"5":{"start":{"line":45,"column":8},"end":{"line":45,"column":39}},"6":{"start":{"line":47,"column":8},"end":{"line":47,"column":36}},"7":{"start":{"line":49,"column":8},"end":{"line":49,"column":38}},"8":{"start":{"line":51,"column":8},"end":{"line":51,"column":33}},"9":{"start":{"line":52,"column":8},"end":{"line":52,"column":31}},"10":{"start":{"line":55,"column":8},"end":{"line":65,"column":9}},"11":{"start":{"line":57,"column":29},"end":{"line":57,"column":36}},"12":{"start":{"line":58,"column":12},"end":{"line":58,"column":44}},"13":{"start":{"line":59,"column":12},"end":{"line":59,"column":25}},"14":{"start":{"line":60,"column":15},"end":{"line":65,"column":9}},"15":{"start":{"line":61,"column":12},"end":{"line":61,"column":49}},"16":{"start":{"line":62,"column":12},"end":{"line":64,"column":13}},"17":{"start":{"line":63,"column":16},"end":{"line":63,"column":58}},"18":{"start":{"line":67,"column":8},"end":{"line":67,"column":31}},"19":{"start":{"line":68,"column":8},"end":{"line":68,"column":54}},"20":{"start":{"line":87,"column":8},"end":{"line":87,"column":91}},"21":{"start":{"line":87,"column":46},"end":{"line":87,"column":91}},"22":{"start":{"line":90,"column":28},"end":{"line":90,"column":56}},"23":{"start":{"line":95,"column":8},"end":{"line":116,"column":9}},"24":{"start":{"line":97,"column":12},"end":{"line":97,"column":41}},"25":{"start":{"line":98,"column":12},"end":{"line":103,"column":13}},"26":{"start":{"line":99,"column":16},"end":{"line":99,"column":54}},"27":{"start":{"line":100,"column":16},"end":{"line":100,"column":30}},"28":{"start":{"line":101,"column":19},"end":{"line":103,"column":13}},"29":{"start":{"line":102,"column":16},"end":{"line":102,"column":38}},"30":{"start":{"line":104,"column":15},"end":{"line":116,"column":9}},"31":{"start":{"line":106,"column":12},"end":{"line":110,"column":14}},"32":{"start":{"line":111,"column":12},"end":{"line":113,"column":13}},"33":{"start":{"line":112,"column":16},"end":{"line":112,"column":38}},"34":{"start":{"line":115,"column":12},"end":{"line":115,"column":73}},"35":{"start":{"line":119,"column":8},"end":{"line":121,"column":9}},"36":{"start":{"line":120,"column":12},"end":{"line":120,"column":39}},"37":{"start":{"line":123,"column":8},"end":{"line":125,"column":11}},"38":{"start":{"line":124,"column":12},"end":{"line":124,"column":34}},"39":{"start":{"line":127,"column":8},"end":{"line":127,"column":48}},"40":{"start":{"line":128,"column":8},"end":{"line":128,"column":20}},"41":{"start":{"line":140,"column":8},"end":{"line":140,"column":47}},"42":{"start":{"line":141,"column":8},"end":{"line":141,"column":20}},"43":{"start":{"line":154,"column":8},"end":{"line":157,"column":9}},"44":{"start":{"line":155,"column":12},"end":{"line":155,"column":60}},"45":{"start":{"line":156,"column":12},"end":{"line":156,"column":24}},"46":{"start":{"line":158,"column":8},"end":{"line":158,"column":51}},"47":{"start":{"line":158,"column":22},"end":{"line":158,"column":51}},"48":{"start":{"line":159,"column":8},"end":{"line":159,"column":31}},"49":{"start":{"line":160,"column":8},"end":{"line":160,"column":32}},"50":{"start":{"line":161,"column":8},"end":{"line":161,"column":61}},"51":{"start":{"line":161,"column":42},"end":{"line":161,"column":61}},"52":{"start":{"line":162,"column":8},"end":{"line":162,"column":20}},"53":{"start":{"line":173,"column":8},"end":{"line":173,"column":45}},"54":{"start":{"line":173,"column":33},"end":{"line":173,"column":45}},"55":{"start":{"line":174,"column":8},"end":{"line":174,"column":97}},"56":{"start":{"line":178,"column":8},"end":{"line":178,"column":90}},"57":{"start":{"line":179,"column":8},"end":{"line":179,"column":20}},"58":{"start":{"line":183,"column":8},"end":{"line":183,"column":92}},"59":{"start":{"line":184,"column":8},"end":{"line":184,"column":20}},"60":{"start":{"line":191,"column":8},"end":{"line":197,"column":11}},"61":{"start":{"line":192,"column":12},"end":{"line":192,"column":44}},"62":{"start":{"line":192,"column":37},"end":{"line":192,"column":44}},"63":{"start":{"line":193,"column":12},"end":{"line":193,"column":61}},"64":{"start":{"line":194,"column":12},"end":{"line":194,"column":55}},"65":{"start":{"line":195,"column":12},"end":{"line":195,"column":59}},"66":{"start":{"line":196,"column":12},"end":{"line":196,"column":35}},"67":{"start":{"line":198,"column":8},"end":{"line":202,"column":11}},"68":{"start":{"line":199,"column":12},"end":{"line":199,"column":44}},"69":{"start":{"line":199,"column":37},"end":{"line":199,"column":44}},"70":{"start":{"line":200,"column":12},"end":{"line":200,"column":25}},"71":{"start":{"line":201,"column":12},"end":{"line":201,"column":42}},"72":{"start":{"line":203,"column":8},"end":{"line":208,"column":11}},"73":{"start":{"line":204,"column":12},"end":{"line":204,"column":44}},"74":{"start":{"line":204,"column":37},"end":{"line":204,"column":44}},"75":{"start":{"line":205,"column":30},"end":{"line":205,"column":57}},"76":{"start":{"line":206,"column":12},"end":{"line":206,"column":43}},"77":{"start":{"line":207,"column":12},"end":{"line":207,"column":47}},"78":{"start":{"line":215,"column":8},"end":{"line":215,"column":39}},"79":{"start":{"line":216,"column":8},"end":{"line":216,"column":36}},"80":{"start":{"line":217,"column":8},"end":{"line":217,"column":38}},"81":{"start":{"line":226,"column":8},"end":{"line":229,"column":11}},"82":{"start":{"line":227,"column":12},"end":{"line":227,"column":45}},"83":{"start":{"line":228,"column":12},"end":{"line":228,"column":84}},"84":{"start":{"line":228,"column":65},"end":{"line":228,"column":84}},"85":{"start":{"line":230,"column":8},"end":{"line":230,"column":38}},"86":{"start":{"line":239,"column":26},"end":{"line":239,"column":38}},"87":{"start":{"line":240,"column":8},"end":{"line":240,"column":34}},"88":{"start":{"line":241,"column":8},"end":{"line":241,"column":49}},"89":{"start":{"line":244,"column":8},"end":{"line":261,"column":9}},"90":{"start":{"line":245,"column":12},"end":{"line":247,"column":13}},"91":{"start":{"line":246,"column":16},"end":{"line":246,"column":66}},"92":{"start":{"line":249,"column":12},"end":{"line":252,"column":13}},"93":{"start":{"line":250,"column":39},"end":{"line":250,"column":85}},"94":{"start":{"line":251,"column":16},"end":{"line":251,"column":86}},"95":{"start":{"line":254,"column":12},"end":{"line":256,"column":13}},"96":{"start":{"line":255,"column":16},"end":{"line":255,"column":76}},"97":{"start":{"line":258,"column":12},"end":{"line":260,"column":13}},"98":{"start":{"line":259,"column":16},"end":{"line":259,"column":34}},"99":{"start":{"line":263,"column":8},"end":{"line":263,"column":25}},"100":{"start":{"line":272,"column":8},"end":{"line":274,"column":9}},"101":{"start":{"line":273,"column":12},"end":{"line":273,"column":59}},"102":{"start":{"line":276,"column":8},"end":{"line":279,"column":9}},"103":{"start":{"line":277,"column":35},"end":{"line":277,"column":81}},"104":{"start":{"line":278,"column":12},"end":{"line":278,"column":79}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":5}},"loc":{"start":{"line":35,"column":45},"end":{"line":69,"column":5}},"line":35},"1":{"name":"(anonymous_1)","decl":{"start":{"line":86,"column":4},"end":{"line":86,"column":5}},"loc":{"start":{"line":86,"column":48},"end":{"line":129,"column":5}},"line":86},"2":{"name":"(anonymous_2)","decl":{"start":{"line":123,"column":31},"end":{"line":123,"column":32}},"loc":{"start":{"line":123,"column":37},"end":{"line":125,"column":9}},"line":123},"3":{"name":"(anonymous_3)","decl":{"start":{"line":139,"column":4},"end":{"line":139,"column":5}},"loc":{"start":{"line":139,"column":29},"end":{"line":142,"column":5}},"line":139},"4":{"name":"(anonymous_4)","decl":{"start":{"line":153,"column":4},"end":{"line":153,"column":5}},"loc":{"start":{"line":153,"column":20},"end":{"line":163,"column":5}},"line":153},"5":{"name":"(anonymous_5)","decl":{"start":{"line":172,"column":4},"end":{"line":172,"column":5}},"loc":{"start":{"line":172,"column":14},"end":{"line":175,"column":5}},"line":172},"6":{"name":"(anonymous_6)","decl":{"start":{"line":177,"column":4},"end":{"line":177,"column":5}},"loc":{"start":{"line":177,"column":10},"end":{"line":180,"column":5}},"line":177},"7":{"name":"(anonymous_7)","decl":{"start":{"line":182,"column":4},"end":{"line":182,"column":5}},"loc":{"start":{"line":182,"column":12},"end":{"line":185,"column":5}},"line":182},"8":{"name":"(anonymous_8)","decl":{"start":{"line":190,"column":4},"end":{"line":190,"column":5}},"loc":{"start":{"line":190,"column":22},"end":{"line":209,"column":5}},"line":190},"9":{"name":"(anonymous_9)","decl":{"start":{"line":191,"column":78},"end":{"line":191,"column":79}},"loc":{"start":{"line":191,"column":87},"end":{"line":197,"column":9}},"line":191},"10":{"name":"(anonymous_10)","decl":{"start":{"line":198,"column":70},"end":{"line":198,"column":71}},"loc":{"start":{"line":198,"column":79},"end":{"line":202,"column":9}},"line":198},"11":{"name":"(anonymous_11)","decl":{"start":{"line":203,"column":81},"end":{"line":203,"column":82}},"loc":{"start":{"line":203,"column":90},"end":{"line":208,"column":9}},"line":203},"12":{"name":"(anonymous_12)","decl":{"start":{"line":214,"column":4},"end":{"line":214,"column":5}},"loc":{"start":{"line":214,"column":23},"end":{"line":218,"column":5}},"line":214},"13":{"name":"(anonymous_13)","decl":{"start":{"line":224,"column":4},"end":{"line":224,"column":5}},"loc":{"start":{"line":224,"column":27},"end":{"line":231,"column":5}},"line":224},"14":{"name":"(anonymous_14)","decl":{"start":{"line":226,"column":27},"end":{"line":226,"column":28}},"loc":{"start":{"line":226,"column":33},"end":{"line":229,"column":9}},"line":226},"15":{"name":"(anonymous_15)","decl":{"start":{"line":238,"column":4},"end":{"line":238,"column":5}},"loc":{"start":{"line":238,"column":23},"end":{"line":264,"column":5}},"line":238},"16":{"name":"(anonymous_16)","decl":{"start":{"line":271,"column":4},"end":{"line":271,"column":5}},"loc":{"start":{"line":271,"column":32},"end":{"line":280,"column":5}},"line":271}},"branchMap":{"0":{"loc":{"start":{"line":55,"column":8},"end":{"line":65,"column":9}},"type":"if","locations":[{"start":{"line":55,"column":8},"end":{"line":65,"column":9}},{"start":{"line":55,"column":8},"end":{"line":65,"column":9}}],"line":55},"1":{"loc":{"start":{"line":60,"column":15},"end":{"line":65,"column":9}},"type":"if","locations":[{"start":{"line":60,"column":15},"end":{"line":65,"column":9}},{"start":{"line":60,"column":15},"end":{"line":65,"column":9}}],"line":60},"2":{"loc":{"start":{"line":60,"column":19},"end":{"line":60,"column":57}},"type":"binary-expr","locations":[{"start":{"line":60,"column":19},"end":{"line":60,"column":26}},{"start":{"line":60,"column":30},"end":{"line":60,"column":57}}],"line":60},"3":{"loc":{"start":{"line":62,"column":12},"end":{"line":64,"column":13}},"type":"if","locations":[{"start":{"line":62,"column":12},"end":{"line":64,"column":13}},{"start":{"line":62,"column":12},"end":{"line":64,"column":13}}],"line":62},"4":{"loc":{"start":{"line":87,"column":8},"end":{"line":87,"column":91}},"type":"if","locations":[{"start":{"line":87,"column":8},"end":{"line":87,"column":91}},{"start":{"line":87,"column":8},"end":{"line":87,"column":91}}],"line":87},"5":{"loc":{"start":{"line":95,"column":8},"end":{"line":116,"column":9}},"type":"if","locations":[{"start":{"line":95,"column":8},"end":{"line":116,"column":9}},{"start":{"line":95,"column":8},"end":{"line":116,"column":9}}],"line":95},"6":{"loc":{"start":{"line":98,"column":12},"end":{"line":103,"column":13}},"type":"if","locations":[{"start":{"line":98,"column":12},"end":{"line":103,"column":13}},{"start":{"line":98,"column":12},"end":{"line":103,"column":13}}],"line":98},"7":{"loc":{"start":{"line":101,"column":19},"end":{"line":103,"column":13}},"type":"if","locations":[{"start":{"line":101,"column":19},"end":{"line":103,"column":13}},{"start":{"line":101,"column":19},"end":{"line":103,"column":13}}],"line":101},"8":{"loc":{"start":{"line":104,"column":15},"end":{"line":116,"column":9}},"type":"if","locations":[{"start":{"line":104,"column":15},"end":{"line":116,"column":9}},{"start":{"line":104,"column":15},"end":{"line":116,"column":9}}],"line":104},"9":{"loc":{"start":{"line":108,"column":22},"end":{"line":108,"column":47}},"type":"binary-expr","locations":[{"start":{"line":108,"column":22},"end":{"line":108,"column":34}},{"start":{"line":108,"column":38},"end":{"line":108,"column":47}}],"line":108},"10":{"loc":{"start":{"line":111,"column":12},"end":{"line":113,"column":13}},"type":"if","locations":[{"start":{"line":111,"column":12},"end":{"line":113,"column":13}},{"start":{"line":111,"column":12},"end":{"line":113,"column":13}}],"line":111},"11":{"loc":{"start":{"line":119,"column":8},"end":{"line":121,"column":9}},"type":"if","locations":[{"start":{"line":119,"column":8},"end":{"line":121,"column":9}},{"start":{"line":119,"column":8},"end":{"line":121,"column":9}}],"line":119},"12":{"loc":{"start":{"line":154,"column":8},"end":{"line":157,"column":9}},"type":"if","locations":[{"start":{"line":154,"column":8},"end":{"line":157,"column":9}},{"start":{"line":154,"column":8},"end":{"line":157,"column":9}}],"line":154},"13":{"loc":{"start":{"line":158,"column":8},"end":{"line":158,"column":51}},"type":"if","locations":[{"start":{"line":158,"column":8},"end":{"line":158,"column":51}},{"start":{"line":158,"column":8},"end":{"line":158,"column":51}}],"line":158},"14":{"loc":{"start":{"line":161,"column":8},"end":{"line":161,"column":61}},"type":"if","locations":[{"start":{"line":161,"column":8},"end":{"line":161,"column":61}},{"start":{"line":161,"column":8},"end":{"line":161,"column":61}}],"line":161},"15":{"loc":{"start":{"line":173,"column":8},"end":{"line":173,"column":45}},"type":"if","locations":[{"start":{"line":173,"column":8},"end":{"line":173,"column":45}},{"start":{"line":173,"column":8},"end":{"line":173,"column":45}}],"line":173},"16":{"loc":{"start":{"line":192,"column":12},"end":{"line":192,"column":44}},"type":"if","locations":[{"start":{"line":192,"column":12},"end":{"line":192,"column":44}},{"start":{"line":192,"column":12},"end":{"line":192,"column":44}}],"line":192},"17":{"loc":{"start":{"line":199,"column":12},"end":{"line":199,"column":44}},"type":"if","locations":[{"start":{"line":199,"column":12},"end":{"line":199,"column":44}},{"start":{"line":199,"column":12},"end":{"line":199,"column":44}}],"line":199},"18":{"loc":{"start":{"line":204,"column":12},"end":{"line":204,"column":44}},"type":"if","locations":[{"start":{"line":204,"column":12},"end":{"line":204,"column":44}},{"start":{"line":204,"column":12},"end":{"line":204,"column":44}}],"line":204},"19":{"loc":{"start":{"line":228,"column":12},"end":{"line":228,"column":84}},"type":"if","locations":[{"start":{"line":228,"column":12},"end":{"line":228,"column":84}},{"start":{"line":228,"column":12},"end":{"line":228,"column":84}}],"line":228},"20":{"loc":{"start":{"line":228,"column":16},"end":{"line":228,"column":63}},"type":"binary-expr","locations":[{"start":{"line":228,"column":16},"end":{"line":228,"column":31}},{"start":{"line":228,"column":35},"end":{"line":228,"column":63}}],"line":228},"21":{"loc":{"start":{"line":244,"column":8},"end":{"line":261,"column":9}},"type":"if","locations":[{"start":{"line":244,"column":8},"end":{"line":261,"column":9}},{"start":{"line":244,"column":8},"end":{"line":261,"column":9}}],"line":244},"22":{"loc":{"start":{"line":245,"column":12},"end":{"line":247,"column":13}},"type":"if","locations":[{"start":{"line":245,"column":12},"end":{"line":247,"column":13}},{"start":{"line":245,"column":12},"end":{"line":247,"column":13}}],"line":245},"23":{"loc":{"start":{"line":249,"column":12},"end":{"line":252,"column":13}},"type":"if","locations":[{"start":{"line":249,"column":12},"end":{"line":252,"column":13}},{"start":{"line":249,"column":12},"end":{"line":252,"column":13}}],"line":249},"24":{"loc":{"start":{"line":250,"column":39},"end":{"line":250,"column":85}},"type":"binary-expr","locations":[{"start":{"line":250,"column":39},"end":{"line":250,"column":80}},{"start":{"line":250,"column":84},"end":{"line":250,"column":85}}],"line":250},"25":{"loc":{"start":{"line":254,"column":12},"end":{"line":256,"column":13}},"type":"if","locations":[{"start":{"line":254,"column":12},"end":{"line":256,"column":13}},{"start":{"line":254,"column":12},"end":{"line":256,"column":13}}],"line":254},"26":{"loc":{"start":{"line":258,"column":12},"end":{"line":260,"column":13}},"type":"if","locations":[{"start":{"line":258,"column":12},"end":{"line":260,"column":13}},{"start":{"line":258,"column":12},"end":{"line":260,"column":13}}],"line":258},"27":{"loc":{"start":{"line":272,"column":8},"end":{"line":274,"column":9}},"type":"if","locations":[{"start":{"line":272,"column":8},"end":{"line":274,"column":9}},{"start":{"line":272,"column":8},"end":{"line":274,"column":9}}],"line":272},"28":{"loc":{"start":{"line":276,"column":8},"end":{"line":279,"column":9}},"type":"if","locations":[{"start":{"line":276,"column":8},"end":{"line":279,"column":9}},{"start":{"line":276,"column":8},"end":{"line":279,"column":9}}],"line":276},"29":{"loc":{"start":{"line":277,"column":35},"end":{"line":277,"column":81}},"type":"binary-expr","locations":[{"start":{"line":277,"column":35},"end":{"line":277,"column":76}},{"start":{"line":277,"column":80},"end":{"line":277,"column":81}}],"line":277}},"s":{"0":3,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":6,"8":6,"9":6,"10":6,"11":1,"12":1,"13":1,"14":5,"15":4,"16":4,"17":1,"18":6,"19":6,"20":3,"21":0,"22":3,"23":3,"24":3,"25":3,"26":0,"27":0,"28":3,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":3,"36":0,"37":3,"38":0,"39":3,"40":3,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":6,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":6,"68":0,"69":0,"70":0,"71":0,"72":6,"73":4,"74":1,"75":3,"76":3,"77":3,"78":0,"79":0,"80":0,"81":3,"82":0,"83":0,"84":0,"85":3,"86":3,"87":3,"88":3,"89":3,"90":3,"91":0,"92":3,"93":0,"94":0,"95":3,"96":1,"97":3,"98":1,"99":3,"100":0,"101":0,"102":0,"103":0,"104":0},"f":{"0":6,"1":3,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":6,"9":0,"10":0,"11":4,"12":0,"13":3,"14":0,"15":3,"16":0},"b":{"0":[1,5],"1":[4,1],"2":[5,4],"3":[1,3],"4":[0,3],"5":[3,0],"6":[0,3],"7":[0,3],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,3],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0],"18":[1,3],"19":[0,0],"20":[0,0],"21":[3,0],"22":[0,3],"23":[0,3],"24":[0,0],"25":[1,2],"26":[1,2],"27":[0,0],"28":[0,0],"29":[0,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"effed594790b54edc151c34d5573a4ebd10765a4"} -,"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Socket.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/Socket.js","statementMap":{"0":{"start":{"line":51,"column":8},"end":{"line":51,"column":16}},"1":{"start":{"line":53,"column":8},"end":{"line":53,"column":31}},"2":{"start":{"line":55,"column":8},"end":{"line":55,"column":48}},"3":{"start":{"line":57,"column":8},"end":{"line":57,"column":49}},"4":{"start":{"line":59,"column":8},"end":{"line":59,"column":31}},"5":{"start":{"line":61,"column":8},"end":{"line":61,"column":34}},"6":{"start":{"line":63,"column":8},"end":{"line":63,"column":35}},"7":{"start":{"line":65,"column":8},"end":{"line":65,"column":24}},"8":{"start":{"line":67,"column":8},"end":{"line":67,"column":57}},"9":{"start":{"line":69,"column":8},"end":{"line":69,"column":32}},"10":{"start":{"line":71,"column":8},"end":{"line":71,"column":29}},"11":{"start":{"line":73,"column":8},"end":{"line":73,"column":31}},"12":{"start":{"line":75,"column":8},"end":{"line":75,"column":34}},"13":{"start":{"line":77,"column":8},"end":{"line":77,"column":28}},"14":{"start":{"line":79,"column":8},"end":{"line":79,"column":31}},"15":{"start":{"line":81,"column":8},"end":{"line":81,"column":33}},"16":{"start":{"line":83,"column":8},"end":{"line":83,"column":29}},"17":{"start":{"line":85,"column":8},"end":{"line":85,"column":32}},"18":{"start":{"line":88,"column":8},"end":{"line":88,"column":34}},"19":{"start":{"line":90,"column":8},"end":{"line":90,"column":36}},"20":{"start":{"line":91,"column":8},"end":{"line":91,"column":43}},"21":{"start":{"line":92,"column":8},"end":{"line":92,"column":43}},"22":{"start":{"line":93,"column":8},"end":{"line":93,"column":39}},"23":{"start":{"line":94,"column":8},"end":{"line":94,"column":38}},"24":{"start":{"line":95,"column":8},"end":{"line":95,"column":35}},"25":{"start":{"line":96,"column":8},"end":{"line":96,"column":39}},"26":{"start":{"line":97,"column":8},"end":{"line":97,"column":36}},"27":{"start":{"line":98,"column":8},"end":{"line":98,"column":38}},"28":{"start":{"line":99,"column":8},"end":{"line":99,"column":35}},"29":{"start":{"line":100,"column":8},"end":{"line":100,"column":31}},"30":{"start":{"line":104,"column":8},"end":{"line":104,"column":32}},"31":{"start":{"line":108,"column":8},"end":{"line":108,"column":31}},"32":{"start":{"line":112,"column":8},"end":{"line":112,"column":29}},"33":{"start":{"line":116,"column":8},"end":{"line":116,"column":32}},"34":{"start":{"line":120,"column":8},"end":{"line":120,"column":34}},"35":{"start":{"line":124,"column":8},"end":{"line":124,"column":31}},"36":{"start":{"line":128,"column":8},"end":{"line":128,"column":29}},"37":{"start":{"line":136,"column":8},"end":{"line":136,"column":22}},"38":{"start":{"line":137,"column":8},"end":{"line":137,"column":31}},"39":{"start":{"line":145,"column":8},"end":{"line":145,"column":33}},"40":{"start":{"line":146,"column":8},"end":{"line":146,"column":34}},"41":{"start":{"line":147,"column":8},"end":{"line":147,"column":30}},"42":{"start":{"line":148,"column":8},"end":{"line":148,"column":56}},"43":{"start":{"line":149,"column":8},"end":{"line":149,"column":50}},"44":{"start":{"line":150,"column":8},"end":{"line":150,"column":58}},"45":{"start":{"line":151,"column":8},"end":{"line":151,"column":56}},"46":{"start":{"line":152,"column":8},"end":{"line":152,"column":52}},"47":{"start":{"line":160,"column":30},"end":{"line":160,"column":44}},"48":{"start":{"line":162,"column":8},"end":{"line":162,"column":63}},"49":{"start":{"line":163,"column":8},"end":{"line":163,"column":61}},"50":{"start":{"line":164,"column":8},"end":{"line":166,"column":11}},"51":{"start":{"line":165,"column":12},"end":{"line":165,"column":37}},"52":{"start":{"line":165,"column":26},"end":{"line":165,"column":37}},"53":{"start":{"line":167,"column":8},"end":{"line":167,"column":32}},"54":{"start":{"line":168,"column":8},"end":{"line":168,"column":37}},"55":{"start":{"line":169,"column":8},"end":{"line":174,"column":10}},"56":{"start":{"line":175,"column":8},"end":{"line":175,"column":20}},"57":{"start":{"line":192,"column":8},"end":{"line":192,"column":37}},"58":{"start":{"line":193,"column":8},"end":{"line":197,"column":9}},"59":{"start":{"line":194,"column":12},"end":{"line":194,"column":33}},"60":{"start":{"line":196,"column":12},"end":{"line":196,"column":33}},"61":{"start":{"line":198,"column":8},"end":{"line":198,"column":53}},"62":{"start":{"line":198,"column":22},"end":{"line":198,"column":53}},"63":{"start":{"line":199,"column":8},"end":{"line":199,"column":20}},"64":{"start":{"line":206,"column":8},"end":{"line":212,"column":9}},"65":{"start":{"line":207,"column":12},"end":{"line":207,"column":33}},"66":{"start":{"line":208,"column":12},"end":{"line":211,"column":35}},"67":{"start":{"line":209,"column":16},"end":{"line":209,"column":37}},"68":{"start":{"line":210,"column":16},"end":{"line":210,"column":37}},"69":{"start":{"line":219,"column":8},"end":{"line":222,"column":9}},"70":{"start":{"line":220,"column":12},"end":{"line":220,"column":40}},"71":{"start":{"line":221,"column":12},"end":{"line":221,"column":38}},"72":{"start":{"line":235,"column":8},"end":{"line":235,"column":34}},"73":{"start":{"line":236,"column":8},"end":{"line":236,"column":20}},"74":{"start":{"line":249,"column":8},"end":{"line":252,"column":9}},"75":{"start":{"line":250,"column":12},"end":{"line":250,"column":65}},"76":{"start":{"line":250,"column":39},"end":{"line":250,"column":63}},"77":{"start":{"line":251,"column":12},"end":{"line":251,"column":24}},"78":{"start":{"line":253,"column":8},"end":{"line":253,"column":63}},"79":{"start":{"line":254,"column":8},"end":{"line":254,"column":20}},"80":{"start":{"line":266,"column":8},"end":{"line":269,"column":9}},"81":{"start":{"line":267,"column":12},"end":{"line":267,"column":80}},"82":{"start":{"line":267,"column":39},"end":{"line":267,"column":78}},"83":{"start":{"line":268,"column":12},"end":{"line":268,"column":24}},"84":{"start":{"line":271,"column":8},"end":{"line":275,"column":9}},"85":{"start":{"line":272,"column":12},"end":{"line":274,"column":14}},"86":{"start":{"line":277,"column":8},"end":{"line":277,"column":90}},"87":{"start":{"line":278,"column":8},"end":{"line":278,"column":20}},"88":{"start":{"line":288,"column":8},"end":{"line":288,"column":42}},"89":{"start":{"line":288,"column":32},"end":{"line":288,"column":42}},"90":{"start":{"line":289,"column":8},"end":{"line":289,"column":95}},"91":{"start":{"line":299,"column":8},"end":{"line":304,"column":9}},"92":{"start":{"line":300,"column":12},"end":{"line":302,"column":15}},"93":{"start":{"line":301,"column":16},"end":{"line":301,"column":55}},"94":{"start":{"line":303,"column":12},"end":{"line":303,"column":24}},"95":{"start":{"line":305,"column":8},"end":{"line":305,"column":58}},"96":{"start":{"line":305,"column":46},"end":{"line":305,"column":58}},"97":{"start":{"line":307,"column":8},"end":{"line":307,"column":29}},"98":{"start":{"line":308,"column":8},"end":{"line":308,"column":47}},"99":{"start":{"line":309,"column":8},"end":{"line":309,"column":20}},"100":{"start":{"line":316,"column":8},"end":{"line":316,"column":41}},"101":{"start":{"line":316,"column":29},"end":{"line":316,"column":41}},"102":{"start":{"line":317,"column":8},"end":{"line":317,"column":31}},"103":{"start":{"line":318,"column":8},"end":{"line":318,"column":29}},"104":{"start":{"line":319,"column":8},"end":{"line":319,"column":51}},"105":{"start":{"line":320,"column":8},"end":{"line":320,"column":20}},"106":{"start":{"line":338,"column":8},"end":{"line":338,"column":83}},"107":{"start":{"line":338,"column":46},"end":{"line":338,"column":83}},"108":{"start":{"line":340,"column":32},"end":{"line":340,"column":74}},"109":{"start":{"line":341,"column":8},"end":{"line":341,"column":60}},"110":{"start":{"line":342,"column":29},"end":{"line":342,"column":40}},"111":{"start":{"line":343,"column":8},"end":{"line":343,"column":66}},"112":{"start":{"line":344,"column":30},"end":{"line":360,"column":9}},"113":{"start":{"line":345,"column":35},"end":{"line":345,"column":38}},"114":{"start":{"line":346,"column":12},"end":{"line":359,"column":13}},"115":{"start":{"line":347,"column":16},"end":{"line":347,"column":77}},"116":{"start":{"line":348,"column":16},"end":{"line":348,"column":68}},"117":{"start":{"line":349,"column":16},"end":{"line":349,"column":43}},"118":{"start":{"line":350,"column":16},"end":{"line":350,"column":37}},"119":{"start":{"line":351,"column":16},"end":{"line":354,"column":17}},"120":{"start":{"line":352,"column":20},"end":{"line":352,"column":51}},"121":{"start":{"line":353,"column":20},"end":{"line":353,"column":39}},"122":{"start":{"line":355,"column":16},"end":{"line":358,"column":17}},"123":{"start":{"line":356,"column":20},"end":{"line":357,"column":30}},"124":{"start":{"line":356,"column":29},"end":{"line":356,"column":48}},"125":{"start":{"line":357,"column":25},"end":{"line":357,"column":30}},"126":{"start":{"line":362,"column":8},"end":{"line":362,"column":63}},"127":{"start":{"line":363,"column":19},"end":{"line":363,"column":69}},"128":{"start":{"line":364,"column":8},"end":{"line":364,"column":47}},"129":{"start":{"line":364,"column":17},"end":{"line":364,"column":47}},"130":{"start":{"line":365,"column":8},"end":{"line":365,"column":43}},"131":{"start":{"line":366,"column":8},"end":{"line":366,"column":57}},"132":{"start":{"line":367,"column":8},"end":{"line":367,"column":99}},"133":{"start":{"line":368,"column":8},"end":{"line":368,"column":18}},"134":{"start":{"line":375,"column":8},"end":{"line":375,"column":38}},"135":{"start":{"line":375,"column":26},"end":{"line":375,"column":38}},"136":{"start":{"line":376,"column":8},"end":{"line":376,"column":28}},"137":{"start":{"line":377,"column":8},"end":{"line":377,"column":49}},"138":{"start":{"line":378,"column":8},"end":{"line":378,"column":27}},"139":{"start":{"line":379,"column":8},"end":{"line":379,"column":20}},"140":{"start":{"line":386,"column":8},"end":{"line":386,"column":34}},"141":{"start":{"line":386,"column":27},"end":{"line":386,"column":34}},"142":{"start":{"line":387,"column":8},"end":{"line":387,"column":29}},"143":{"start":{"line":388,"column":8},"end":{"line":388,"column":28}},"144":{"start":{"line":389,"column":8},"end":{"line":389,"column":44}},"145":{"start":{"line":393,"column":8},"end":{"line":393,"column":90}},"146":{"start":{"line":397,"column":8},"end":{"line":397,"column":92}},"147":{"start":{"line":404,"column":8},"end":{"line":404,"column":35}},"148":{"start":{"line":404,"column":28},"end":{"line":404,"column":35}},"149":{"start":{"line":405,"column":8},"end":{"line":405,"column":30}},"150":{"start":{"line":406,"column":8},"end":{"line":435,"column":9}},"151":{"start":{"line":408,"column":30},"end":{"line":408,"column":32}},"152":{"start":{"line":409,"column":28},"end":{"line":409,"column":29}},"153":{"start":{"line":410,"column":20},"end":{"line":410,"column":21}},"154":{"start":{"line":411,"column":12},"end":{"line":422,"column":13}},"155":{"start":{"line":412,"column":32},"end":{"line":412,"column":85}},"156":{"start":{"line":413,"column":16},"end":{"line":413,"column":48}},"157":{"start":{"line":414,"column":16},"end":{"line":421,"column":17}},"158":{"start":{"line":415,"column":20},"end":{"line":415,"column":44}},"159":{"start":{"line":417,"column":39},"end":{"line":417,"column":77}},"160":{"start":{"line":418,"column":20},"end":{"line":418,"column":65}},"161":{"start":{"line":419,"column":20},"end":{"line":419,"column":98}},"162":{"start":{"line":420,"column":20},"end":{"line":420,"column":26}},"163":{"start":{"line":424,"column":24},"end":{"line":427,"column":13}},"164":{"start":{"line":429,"column":12},"end":{"line":429,"column":69}},"165":{"start":{"line":430,"column":12},"end":{"line":430,"column":39}},"166":{"start":{"line":431,"column":12},"end":{"line":434,"column":13}},"167":{"start":{"line":432,"column":16},"end":{"line":432,"column":39}},"168":{"start":{"line":433,"column":16},"end":{"line":433,"column":23}},"169":{"start":{"line":436,"column":8},"end":{"line":436,"column":31}},"170":{"start":{"line":437,"column":8},"end":{"line":437,"column":50}},"171":{"start":{"line":443,"column":23},"end":{"line":455,"column":5}},"172":{"start":{"line":444,"column":8},"end":{"line":444,"column":40}},"173":{"start":{"line":444,"column":33},"end":{"line":444,"column":40}},"174":{"start":{"line":445,"column":8},"end":{"line":445,"column":29}},"175":{"start":{"line":446,"column":8},"end":{"line":454,"column":9}},"176":{"start":{"line":447,"column":31},"end":{"line":447,"column":62}},"177":{"start":{"line":448,"column":12},"end":{"line":448,"column":53}},"178":{"start":{"line":449,"column":30},"end":{"line":449,"column":95}},"179":{"start":{"line":450,"column":12},"end":{"line":450,"column":41}},"180":{"start":{"line":453,"column":12},"end":{"line":453,"column":45}},"181":{"start":{"line":461,"column":8},"end":{"line":461,"column":33}},"182":{"start":{"line":462,"column":8},"end":{"line":462,"column":91}},"183":{"start":{"line":463,"column":8},"end":{"line":467,"column":11}},"184":{"start":{"line":464,"column":12},"end":{"line":464,"column":44}},"185":{"start":{"line":464,"column":37},"end":{"line":464,"column":44}},"186":{"start":{"line":465,"column":12},"end":{"line":465,"column":27}},"187":{"start":{"line":466,"column":12},"end":{"line":466,"column":42}},"188":{"start":{"line":468,"column":8},"end":{"line":472,"column":11}},"189":{"start":{"line":469,"column":12},"end":{"line":469,"column":44}},"190":{"start":{"line":469,"column":37},"end":{"line":469,"column":44}},"191":{"start":{"line":470,"column":12},"end":{"line":470,"column":36}},"192":{"start":{"line":471,"column":12},"end":{"line":471,"column":42}},"193":{"start":{"line":473,"column":8},"end":{"line":479,"column":11}},"194":{"start":{"line":474,"column":12},"end":{"line":474,"column":44}},"195":{"start":{"line":474,"column":37},"end":{"line":474,"column":44}},"196":{"start":{"line":475,"column":12},"end":{"line":477,"column":13}},"197":{"start":{"line":476,"column":16},"end":{"line":476,"column":27}},"198":{"start":{"line":478,"column":12},"end":{"line":478,"column":29}},"199":{"start":{"line":480,"column":8},"end":{"line":484,"column":11}},"200":{"start":{"line":481,"column":12},"end":{"line":481,"column":44}},"201":{"start":{"line":481,"column":37},"end":{"line":481,"column":44}},"202":{"start":{"line":482,"column":12},"end":{"line":482,"column":47}},"203":{"start":{"line":483,"column":12},"end":{"line":483,"column":33}},"204":{"start":{"line":485,"column":8},"end":{"line":488,"column":11}},"205":{"start":{"line":486,"column":12},"end":{"line":486,"column":44}},"206":{"start":{"line":486,"column":37},"end":{"line":486,"column":44}},"207":{"start":{"line":487,"column":12},"end":{"line":487,"column":53}},"208":{"start":{"line":495,"column":8},"end":{"line":495,"column":37}},"209":{"start":{"line":496,"column":8},"end":{"line":496,"column":38}},"210":{"start":{"line":497,"column":8},"end":{"line":497,"column":38}},"211":{"start":{"line":498,"column":8},"end":{"line":498,"column":36}},"212":{"start":{"line":499,"column":8},"end":{"line":499,"column":40}},"213":{"start":{"line":500,"column":8},"end":{"line":500,"column":40}},"214":{"start":{"line":509,"column":8},"end":{"line":519,"column":9}},"215":{"start":{"line":510,"column":12},"end":{"line":510,"column":49}},"216":{"start":{"line":511,"column":15},"end":{"line":519,"column":9}},"217":{"start":{"line":512,"column":12},"end":{"line":512,"column":26}},"218":{"start":{"line":513,"column":15},"end":{"line":519,"column":9}},"219":{"start":{"line":514,"column":12},"end":{"line":514,"column":39}},"220":{"start":{"line":516,"column":12},"end":{"line":518,"column":14}},"221":{"start":{"line":526,"column":8},"end":{"line":526,"column":33}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":50,"column":4},"end":{"line":50,"column":5}},"loc":{"start":{"line":50,"column":18},"end":{"line":101,"column":5}},"line":50},"1":{"name":"(anonymous_1)","decl":{"start":{"line":103,"column":4},"end":{"line":103,"column":5}},"loc":{"start":{"line":103,"column":21},"end":{"line":105,"column":5}},"line":103},"2":{"name":"(anonymous_2)","decl":{"start":{"line":107,"column":4},"end":{"line":107,"column":5}},"loc":{"start":{"line":107,"column":20},"end":{"line":109,"column":5}},"line":107},"3":{"name":"(anonymous_3)","decl":{"start":{"line":111,"column":4},"end":{"line":111,"column":5}},"loc":{"start":{"line":111,"column":18},"end":{"line":113,"column":5}},"line":111},"4":{"name":"(anonymous_4)","decl":{"start":{"line":115,"column":4},"end":{"line":115,"column":5}},"loc":{"start":{"line":115,"column":21},"end":{"line":117,"column":5}},"line":115},"5":{"name":"(anonymous_5)","decl":{"start":{"line":119,"column":4},"end":{"line":119,"column":5}},"loc":{"start":{"line":119,"column":23},"end":{"line":121,"column":5}},"line":119},"6":{"name":"(anonymous_6)","decl":{"start":{"line":123,"column":4},"end":{"line":123,"column":5}},"loc":{"start":{"line":123,"column":20},"end":{"line":125,"column":5}},"line":123},"7":{"name":"(anonymous_7)","decl":{"start":{"line":127,"column":4},"end":{"line":127,"column":5}},"loc":{"start":{"line":127,"column":18},"end":{"line":129,"column":5}},"line":127},"8":{"name":"(anonymous_8)","decl":{"start":{"line":135,"column":4},"end":{"line":135,"column":5}},"loc":{"start":{"line":135,"column":15},"end":{"line":138,"column":5}},"line":135},"9":{"name":"(anonymous_9)","decl":{"start":{"line":144,"column":4},"end":{"line":144,"column":5}},"loc":{"start":{"line":144,"column":34},"end":{"line":153,"column":5}},"line":144},"10":{"name":"(anonymous_10)","decl":{"start":{"line":159,"column":4},"end":{"line":159,"column":5}},"loc":{"start":{"line":159,"column":31},"end":{"line":176,"column":5}},"line":159},"11":{"name":"(anonymous_11)","decl":{"start":{"line":164,"column":29},"end":{"line":164,"column":30}},"loc":{"start":{"line":164,"column":35},"end":{"line":166,"column":9}},"line":164},"12":{"name":"(anonymous_12)","decl":{"start":{"line":191,"column":4},"end":{"line":191,"column":5}},"loc":{"start":{"line":191,"column":34},"end":{"line":200,"column":5}},"line":191},"13":{"name":"(anonymous_13)","decl":{"start":{"line":205,"column":4},"end":{"line":205,"column":5}},"loc":{"start":{"line":205,"column":20},"end":{"line":213,"column":5}},"line":205},"14":{"name":"(anonymous_14)","decl":{"start":{"line":208,"column":39},"end":{"line":208,"column":40}},"loc":{"start":{"line":208,"column":45},"end":{"line":211,"column":13}},"line":208},"15":{"name":"(anonymous_15)","decl":{"start":{"line":218,"column":4},"end":{"line":218,"column":5}},"loc":{"start":{"line":218,"column":20},"end":{"line":223,"column":5}},"line":218},"16":{"name":"(anonymous_16)","decl":{"start":{"line":234,"column":4},"end":{"line":234,"column":5}},"loc":{"start":{"line":234,"column":26},"end":{"line":237,"column":5}},"line":234},"17":{"name":"(anonymous_17)","decl":{"start":{"line":248,"column":4},"end":{"line":248,"column":5}},"loc":{"start":{"line":248,"column":31},"end":{"line":255,"column":5}},"line":248},"18":{"name":"(anonymous_18)","decl":{"start":{"line":250,"column":33},"end":{"line":250,"column":34}},"loc":{"start":{"line":250,"column":39},"end":{"line":250,"column":63}},"line":250},"19":{"name":"(anonymous_19)","decl":{"start":{"line":265,"column":4},"end":{"line":265,"column":5}},"loc":{"start":{"line":265,"column":51},"end":{"line":279,"column":5}},"line":265},"20":{"name":"(anonymous_20)","decl":{"start":{"line":267,"column":33},"end":{"line":267,"column":34}},"loc":{"start":{"line":267,"column":39},"end":{"line":267,"column":78}},"line":267},"21":{"name":"(anonymous_21)","decl":{"start":{"line":287,"column":4},"end":{"line":287,"column":5}},"loc":{"start":{"line":287,"column":14},"end":{"line":290,"column":5}},"line":287},"22":{"name":"(anonymous_22)","decl":{"start":{"line":298,"column":4},"end":{"line":298,"column":5}},"loc":{"start":{"line":298,"column":24},"end":{"line":310,"column":5}},"line":298},"23":{"name":"(anonymous_23)","decl":{"start":{"line":300,"column":39},"end":{"line":300,"column":40}},"loc":{"start":{"line":300,"column":45},"end":{"line":302,"column":13}},"line":300},"24":{"name":"(anonymous_24)","decl":{"start":{"line":315,"column":4},"end":{"line":315,"column":5}},"loc":{"start":{"line":315,"column":14},"end":{"line":321,"column":5}},"line":315},"25":{"name":"(anonymous_25)","decl":{"start":{"line":337,"column":4},"end":{"line":337,"column":5}},"loc":{"start":{"line":337,"column":32},"end":{"line":369,"column":5}},"line":337},"26":{"name":"(anonymous_26)","decl":{"start":{"line":344,"column":30},"end":{"line":344,"column":31}},"loc":{"start":{"line":344,"column":96},"end":{"line":360,"column":9}},"line":344},"27":{"name":"(anonymous_27)","decl":{"start":{"line":374,"column":4},"end":{"line":374,"column":5}},"loc":{"start":{"line":374,"column":12},"end":{"line":380,"column":5}},"line":374},"28":{"name":"(anonymous_28)","decl":{"start":{"line":385,"column":4},"end":{"line":385,"column":5}},"loc":{"start":{"line":385,"column":13},"end":{"line":390,"column":5}},"line":385},"29":{"name":"(anonymous_29)","decl":{"start":{"line":392,"column":4},"end":{"line":392,"column":5}},"loc":{"start":{"line":392,"column":10},"end":{"line":394,"column":5}},"line":392},"30":{"name":"(anonymous_30)","decl":{"start":{"line":396,"column":4},"end":{"line":396,"column":5}},"loc":{"start":{"line":396,"column":12},"end":{"line":398,"column":5}},"line":396},"31":{"name":"(anonymous_31)","decl":{"start":{"line":403,"column":4},"end":{"line":403,"column":5}},"loc":{"start":{"line":403,"column":41},"end":{"line":438,"column":5}},"line":403},"32":{"name":"(anonymous_32)","decl":{"start":{"line":443,"column":23},"end":{"line":443,"column":24}},"loc":{"start":{"line":443,"column":77},"end":{"line":455,"column":5}},"line":443},"33":{"name":"(anonymous_33)","decl":{"start":{"line":460,"column":4},"end":{"line":460,"column":5}},"loc":{"start":{"line":460,"column":22},"end":{"line":489,"column":5}},"line":460},"34":{"name":"(anonymous_34)","decl":{"start":{"line":463,"column":70},"end":{"line":463,"column":71}},"loc":{"start":{"line":463,"column":79},"end":{"line":467,"column":9}},"line":463},"35":{"name":"(anonymous_35)","decl":{"start":{"line":468,"column":70},"end":{"line":468,"column":71}},"loc":{"start":{"line":468,"column":79},"end":{"line":472,"column":9}},"line":468},"36":{"name":"(anonymous_36)","decl":{"start":{"line":473,"column":66},"end":{"line":473,"column":67}},"loc":{"start":{"line":473,"column":75},"end":{"line":479,"column":9}},"line":473},"37":{"name":"(anonymous_37)","decl":{"start":{"line":480,"column":74},"end":{"line":480,"column":75}},"loc":{"start":{"line":480,"column":83},"end":{"line":484,"column":9}},"line":480},"38":{"name":"(anonymous_38)","decl":{"start":{"line":485,"column":74},"end":{"line":485,"column":75}},"loc":{"start":{"line":485,"column":83},"end":{"line":488,"column":9}},"line":485},"39":{"name":"(anonymous_39)","decl":{"start":{"line":494,"column":4},"end":{"line":494,"column":5}},"loc":{"start":{"line":494,"column":24},"end":{"line":501,"column":5}},"line":494},"40":{"name":"(anonymous_40)","decl":{"start":{"line":508,"column":4},"end":{"line":508,"column":5}},"loc":{"start":{"line":508,"column":42},"end":{"line":520,"column":5}},"line":508},"41":{"name":"(anonymous_41)","decl":{"start":{"line":525,"column":4},"end":{"line":525,"column":5}},"loc":{"start":{"line":525,"column":23},"end":{"line":527,"column":5}},"line":525}},"branchMap":{"0":{"loc":{"start":{"line":162,"column":29},"end":{"line":162,"column":62}},"type":"binary-expr","locations":[{"start":{"line":162,"column":29},"end":{"line":162,"column":47}},{"start":{"line":162,"column":51},"end":{"line":162,"column":62}}],"line":162},"1":{"loc":{"start":{"line":163,"column":29},"end":{"line":163,"column":60}},"type":"binary-expr","locations":[{"start":{"line":163,"column":29},"end":{"line":163,"column":55}},{"start":{"line":163,"column":59},"end":{"line":163,"column":60}}],"line":163},"2":{"loc":{"start":{"line":165,"column":12},"end":{"line":165,"column":37}},"type":"if","locations":[{"start":{"line":165,"column":12},"end":{"line":165,"column":37}},{"start":{"line":165,"column":12},"end":{"line":165,"column":37}}],"line":165},"3":{"loc":{"start":{"line":193,"column":8},"end":{"line":197,"column":9}},"type":"if","locations":[{"start":{"line":193,"column":8},"end":{"line":197,"column":9}},{"start":{"line":193,"column":8},"end":{"line":197,"column":9}}],"line":193},"4":{"loc":{"start":{"line":198,"column":8},"end":{"line":198,"column":53}},"type":"if","locations":[{"start":{"line":198,"column":8},"end":{"line":198,"column":53}},{"start":{"line":198,"column":8},"end":{"line":198,"column":53}}],"line":198},"5":{"loc":{"start":{"line":206,"column":8},"end":{"line":212,"column":9}},"type":"if","locations":[{"start":{"line":206,"column":8},"end":{"line":212,"column":9}},{"start":{"line":206,"column":8},"end":{"line":212,"column":9}}],"line":206},"6":{"loc":{"start":{"line":219,"column":8},"end":{"line":222,"column":9}},"type":"if","locations":[{"start":{"line":219,"column":8},"end":{"line":222,"column":9}},{"start":{"line":219,"column":8},"end":{"line":222,"column":9}}],"line":219},"7":{"loc":{"start":{"line":248,"column":15},"end":{"line":248,"column":29}},"type":"default-arg","locations":[{"start":{"line":248,"column":25},"end":{"line":248,"column":29}}],"line":248},"8":{"loc":{"start":{"line":249,"column":8},"end":{"line":252,"column":9}},"type":"if","locations":[{"start":{"line":249,"column":8},"end":{"line":252,"column":9}},{"start":{"line":249,"column":8},"end":{"line":252,"column":9}}],"line":249},"9":{"loc":{"start":{"line":265,"column":17},"end":{"line":265,"column":31}},"type":"default-arg","locations":[{"start":{"line":265,"column":26},"end":{"line":265,"column":31}}],"line":265},"10":{"loc":{"start":{"line":265,"column":33},"end":{"line":265,"column":49}},"type":"default-arg","locations":[{"start":{"line":265,"column":48},"end":{"line":265,"column":49}}],"line":265},"11":{"loc":{"start":{"line":266,"column":8},"end":{"line":269,"column":9}},"type":"if","locations":[{"start":{"line":266,"column":8},"end":{"line":269,"column":9}},{"start":{"line":266,"column":8},"end":{"line":269,"column":9}}],"line":266},"12":{"loc":{"start":{"line":271,"column":8},"end":{"line":275,"column":9}},"type":"if","locations":[{"start":{"line":271,"column":8},"end":{"line":275,"column":9}},{"start":{"line":271,"column":8},"end":{"line":275,"column":9}}],"line":271},"13":{"loc":{"start":{"line":288,"column":8},"end":{"line":288,"column":42}},"type":"if","locations":[{"start":{"line":288,"column":8},"end":{"line":288,"column":42}},{"start":{"line":288,"column":8},"end":{"line":288,"column":42}}],"line":288},"14":{"loc":{"start":{"line":299,"column":8},"end":{"line":304,"column":9}},"type":"if","locations":[{"start":{"line":299,"column":8},"end":{"line":304,"column":9}},{"start":{"line":299,"column":8},"end":{"line":304,"column":9}}],"line":299},"15":{"loc":{"start":{"line":305,"column":8},"end":{"line":305,"column":58}},"type":"if","locations":[{"start":{"line":305,"column":8},"end":{"line":305,"column":58}},{"start":{"line":305,"column":8},"end":{"line":305,"column":58}}],"line":305},"16":{"loc":{"start":{"line":305,"column":12},"end":{"line":305,"column":44}},"type":"binary-expr","locations":[{"start":{"line":305,"column":12},"end":{"line":305,"column":25}},{"start":{"line":305,"column":29},"end":{"line":305,"column":44}}],"line":305},"17":{"loc":{"start":{"line":316,"column":8},"end":{"line":316,"column":41}},"type":"if","locations":[{"start":{"line":316,"column":8},"end":{"line":316,"column":41}},{"start":{"line":316,"column":8},"end":{"line":316,"column":41}}],"line":316},"18":{"loc":{"start":{"line":338,"column":8},"end":{"line":338,"column":83}},"type":"if","locations":[{"start":{"line":338,"column":8},"end":{"line":338,"column":83}},{"start":{"line":338,"column":8},"end":{"line":338,"column":83}}],"line":338},"19":{"loc":{"start":{"line":338,"column":12},"end":{"line":338,"column":44}},"type":"binary-expr","locations":[{"start":{"line":338,"column":12},"end":{"line":338,"column":25}},{"start":{"line":338,"column":29},"end":{"line":338,"column":44}}],"line":338},"20":{"loc":{"start":{"line":346,"column":12},"end":{"line":359,"column":13}},"type":"if","locations":[{"start":{"line":346,"column":12},"end":{"line":359,"column":13}},{"start":{"line":346,"column":12},"end":{"line":359,"column":13}}],"line":346},"21":{"loc":{"start":{"line":351,"column":16},"end":{"line":354,"column":17}},"type":"if","locations":[{"start":{"line":351,"column":16},"end":{"line":354,"column":17}},{"start":{"line":351,"column":16},"end":{"line":354,"column":17}}],"line":351},"22":{"loc":{"start":{"line":351,"column":20},"end":{"line":351,"column":75}},"type":"binary-expr","locations":[{"start":{"line":351,"column":20},"end":{"line":351,"column":42}},{"start":{"line":351,"column":46},"end":{"line":351,"column":75}}],"line":351},"23":{"loc":{"start":{"line":355,"column":16},"end":{"line":358,"column":17}},"type":"if","locations":[{"start":{"line":355,"column":16},"end":{"line":358,"column":17}},{"start":{"line":355,"column":16},"end":{"line":358,"column":17}}],"line":355},"24":{"loc":{"start":{"line":356,"column":20},"end":{"line":357,"column":30}},"type":"if","locations":[{"start":{"line":356,"column":20},"end":{"line":357,"column":30}},{"start":{"line":356,"column":20},"end":{"line":357,"column":30}}],"line":356},"25":{"loc":{"start":{"line":364,"column":8},"end":{"line":364,"column":47}},"type":"if","locations":[{"start":{"line":364,"column":8},"end":{"line":364,"column":47}},{"start":{"line":364,"column":8},"end":{"line":364,"column":47}}],"line":364},"26":{"loc":{"start":{"line":375,"column":8},"end":{"line":375,"column":38}},"type":"if","locations":[{"start":{"line":375,"column":8},"end":{"line":375,"column":38}},{"start":{"line":375,"column":8},"end":{"line":375,"column":38}}],"line":375},"27":{"loc":{"start":{"line":386,"column":8},"end":{"line":386,"column":34}},"type":"if","locations":[{"start":{"line":386,"column":8},"end":{"line":386,"column":34}},{"start":{"line":386,"column":8},"end":{"line":386,"column":34}}],"line":386},"28":{"loc":{"start":{"line":404,"column":8},"end":{"line":404,"column":35}},"type":"if","locations":[{"start":{"line":404,"column":8},"end":{"line":404,"column":35}},{"start":{"line":404,"column":8},"end":{"line":404,"column":35}}],"line":404},"29":{"loc":{"start":{"line":414,"column":16},"end":{"line":421,"column":17}},"type":"if","locations":[{"start":{"line":414,"column":16},"end":{"line":421,"column":17}},{"start":{"line":414,"column":16},"end":{"line":421,"column":17}}],"line":414},"30":{"loc":{"start":{"line":431,"column":12},"end":{"line":434,"column":13}},"type":"if","locations":[{"start":{"line":431,"column":12},"end":{"line":434,"column":13}},{"start":{"line":431,"column":12},"end":{"line":434,"column":13}}],"line":431},"31":{"loc":{"start":{"line":444,"column":8},"end":{"line":444,"column":40}},"type":"if","locations":[{"start":{"line":444,"column":8},"end":{"line":444,"column":40}},{"start":{"line":444,"column":8},"end":{"line":444,"column":40}}],"line":444},"32":{"loc":{"start":{"line":446,"column":8},"end":{"line":454,"column":9}},"type":"if","locations":[{"start":{"line":446,"column":8},"end":{"line":454,"column":9}},{"start":{"line":446,"column":8},"end":{"line":454,"column":9}}],"line":446},"33":{"loc":{"start":{"line":449,"column":30},"end":{"line":449,"column":95}},"type":"cond-expr","locations":[{"start":{"line":449,"column":47},"end":{"line":449,"column":82}},{"start":{"line":449,"column":85},"end":{"line":449,"column":95}}],"line":449},"34":{"loc":{"start":{"line":464,"column":12},"end":{"line":464,"column":44}},"type":"if","locations":[{"start":{"line":464,"column":12},"end":{"line":464,"column":44}},{"start":{"line":464,"column":12},"end":{"line":464,"column":44}}],"line":464},"35":{"loc":{"start":{"line":469,"column":12},"end":{"line":469,"column":44}},"type":"if","locations":[{"start":{"line":469,"column":12},"end":{"line":469,"column":44}},{"start":{"line":469,"column":12},"end":{"line":469,"column":44}}],"line":469},"36":{"loc":{"start":{"line":474,"column":12},"end":{"line":474,"column":44}},"type":"if","locations":[{"start":{"line":474,"column":12},"end":{"line":474,"column":44}},{"start":{"line":474,"column":12},"end":{"line":474,"column":44}}],"line":474},"37":{"loc":{"start":{"line":475,"column":12},"end":{"line":477,"column":13}},"type":"if","locations":[{"start":{"line":475,"column":12},"end":{"line":477,"column":13}},{"start":{"line":475,"column":12},"end":{"line":477,"column":13}}],"line":475},"38":{"loc":{"start":{"line":481,"column":12},"end":{"line":481,"column":44}},"type":"if","locations":[{"start":{"line":481,"column":12},"end":{"line":481,"column":44}},{"start":{"line":481,"column":12},"end":{"line":481,"column":44}}],"line":481},"39":{"loc":{"start":{"line":486,"column":12},"end":{"line":486,"column":44}},"type":"if","locations":[{"start":{"line":486,"column":12},"end":{"line":486,"column":44}},{"start":{"line":486,"column":12},"end":{"line":486,"column":44}}],"line":486},"40":{"loc":{"start":{"line":509,"column":8},"end":{"line":519,"column":9}},"type":"if","locations":[{"start":{"line":509,"column":8},"end":{"line":519,"column":9}},{"start":{"line":509,"column":8},"end":{"line":519,"column":9}}],"line":509},"41":{"loc":{"start":{"line":511,"column":15},"end":{"line":519,"column":9}},"type":"if","locations":[{"start":{"line":511,"column":15},"end":{"line":519,"column":9}},{"start":{"line":511,"column":15},"end":{"line":519,"column":9}}],"line":511},"42":{"loc":{"start":{"line":513,"column":15},"end":{"line":519,"column":9}},"type":"if","locations":[{"start":{"line":513,"column":15},"end":{"line":519,"column":9}},{"start":{"line":513,"column":15},"end":{"line":519,"column":9}}],"line":513},"43":{"loc":{"start":{"line":513,"column":19},"end":{"line":513,"column":72}},"type":"binary-expr","locations":[{"start":{"line":513,"column":19},"end":{"line":513,"column":47}},{"start":{"line":513,"column":51},"end":{"line":513,"column":72}}],"line":513}},"s":{"0":4,"1":4,"2":4,"3":4,"4":4,"5":4,"6":4,"7":4,"8":4,"9":4,"10":4,"11":4,"12":4,"13":4,"14":4,"15":4,"16":4,"17":4,"18":4,"19":4,"20":4,"21":4,"22":4,"23":4,"24":4,"25":4,"26":4,"27":4,"28":4,"29":4,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":3,"38":3,"39":3,"40":3,"41":3,"42":3,"43":3,"44":3,"45":3,"46":3,"47":1,"48":1,"49":1,"50":1,"51":0,"52":0,"53":1,"54":1,"55":1,"56":1,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":0,"68":0,"69":1,"70":0,"71":0,"72":0,"73":0,"74":0,"75":0,"76":0,"77":0,"78":0,"79":0,"80":0,"81":0,"82":0,"83":0,"84":0,"85":0,"86":0,"87":0,"88":0,"89":0,"90":0,"91":1,"92":0,"93":0,"94":0,"95":1,"96":0,"97":1,"98":1,"99":1,"100":0,"101":0,"102":0,"103":0,"104":0,"105":0,"106":0,"107":0,"108":0,"109":0,"110":0,"111":0,"112":0,"113":0,"114":0,"115":0,"116":0,"117":0,"118":0,"119":0,"120":0,"121":0,"122":0,"123":0,"124":0,"125":0,"126":0,"127":0,"128":0,"129":0,"130":0,"131":0,"132":0,"133":0,"134":1,"135":0,"136":1,"137":1,"138":1,"139":1,"140":0,"141":0,"142":0,"143":0,"144":0,"145":0,"146":0,"147":0,"148":0,"149":0,"150":0,"151":0,"152":0,"153":0,"154":0,"155":0,"156":0,"157":0,"158":0,"159":0,"160":0,"161":0,"162":0,"163":0,"164":0,"165":0,"166":0,"167":0,"168":0,"169":0,"170":0,"171":4,"172":0,"173":0,"174":0,"175":0,"176":0,"177":0,"178":0,"179":0,"180":0,"181":7,"182":7,"183":7,"184":0,"185":0,"186":0,"187":0,"188":7,"189":0,"190":0,"191":0,"192":0,"193":7,"194":3,"195":1,"196":2,"197":1,"198":2,"199":7,"200":0,"201":0,"202":0,"203":0,"204":7,"205":0,"206":0,"207":0,"208":7,"209":7,"210":7,"211":7,"212":7,"213":7,"214":0,"215":0,"216":0,"217":0,"218":0,"219":0,"220":0,"221":0},"f":{"0":4,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":3,"9":3,"10":1,"11":0,"12":0,"13":0,"14":0,"15":1,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":1,"23":0,"24":0,"25":0,"26":0,"27":1,"28":0,"29":0,"30":0,"31":0,"32":0,"33":7,"34":0,"35":0,"36":3,"37":0,"38":0,"39":7,"40":0,"41":0},"b":{"0":[1,0],"1":[1,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,1],"7":[0],"8":[0,0],"9":[0],"10":[0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,1],"15":[0,1],"16":[1,1],"17":[0,0],"18":[0,0],"19":[0,0],"20":[0,0],"21":[0,0],"22":[0,0],"23":[0,0],"24":[0,0],"25":[0,0],"26":[0,1],"27":[0,0],"28":[0,0],"29":[0,0],"30":[0,0],"31":[0,0],"32":[0,0],"33":[0,0],"34":[0,0],"35":[0,0],"36":[1,2],"37":[1,1],"38":[0,0],"39":[0,0],"40":[0,0],"41":[0,0],"42":[0,0],"43":[0,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"a58c8a49b6cc0c7f123cc8a5519c7284089c9099"} -,"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/TLSServer.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/TLSServer.js","statementMap":{"0":{"start":{"line":18,"column":8},"end":{"line":18,"column":16}},"1":{"start":{"line":19,"column":8},"end":{"line":19,"column":92}},"2":{"start":{"line":19,"column":38},"end":{"line":19,"column":92}},"3":{"start":{"line":20,"column":8},"end":{"line":20,"column":34}},"4":{"start":{"line":28,"column":8},"end":{"line":28,"column":39}},"5":{"start":{"line":29,"column":8},"end":{"line":29,"column":86}},"6":{"start":{"line":47,"column":27},"end":{"line":47,"column":41}},"7":{"start":{"line":49,"column":8},"end":{"line":49,"column":42}},"8":{"start":{"line":50,"column":8},"end":{"line":50,"column":50}},"9":{"start":{"line":57,"column":8},"end":{"line":68,"column":10}},"10":{"start":{"line":60,"column":16},"end":{"line":60,"column":48}},"11":{"start":{"line":60,"column":41},"end":{"line":60,"column":48}},"12":{"start":{"line":61,"column":39},"end":{"line":61,"column":66}},"13":{"start":{"line":62,"column":16},"end":{"line":62,"column":51}},"14":{"start":{"line":63,"column":34},"end":{"line":63,"column":63}},"15":{"start":{"line":64,"column":16},"end":{"line":64,"column":47}},"16":{"start":{"line":65,"column":16},"end":{"line":65,"column":56}},"17":{"start":{"line":66,"column":16},"end":{"line":66,"column":57}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":5}},"loc":{"start":{"line":17,"column":42},"end":{"line":21,"column":5}},"line":17},"1":{"name":"(anonymous_1)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":5}},"loc":{"start":{"line":26,"column":30},"end":{"line":30,"column":5}},"line":26},"2":{"name":"(anonymous_2)","decl":{"start":{"line":46,"column":4},"end":{"line":46,"column":5}},"loc":{"start":{"line":46,"column":30},"end":{"line":51,"column":5}},"line":46},"3":{"name":"(anonymous_3)","decl":{"start":{"line":56,"column":4},"end":{"line":56,"column":5}},"loc":{"start":{"line":56,"column":25},"end":{"line":69,"column":5}},"line":56},"4":{"name":"(anonymous_4)","decl":{"start":{"line":59,"column":12},"end":{"line":59,"column":13}},"loc":{"start":{"line":59,"column":21},"end":{"line":67,"column":13}},"line":59}},"branchMap":{"0":{"loc":{"start":{"line":19,"column":8},"end":{"line":19,"column":92}},"type":"if","locations":[{"start":{"line":19,"column":8},"end":{"line":19,"column":92}},{"start":{"line":19,"column":8},"end":{"line":19,"column":92}}],"line":19},"1":{"loc":{"start":{"line":60,"column":16},"end":{"line":60,"column":48}},"type":"if","locations":[{"start":{"line":60,"column":16},"end":{"line":60,"column":48}},{"start":{"line":60,"column":16},"end":{"line":60,"column":48}}],"line":60}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0},"b":{"0":[0,0],"1":[0,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"51eb42643262ddeb31bbd2ad312642ccff802430"} -,"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/TLSSocket.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/TLSSocket.js","statementMap":{"0":{"start":{"line":4,"column":16},"end":{"line":4,"column":40}},"1":{"start":{"line":25,"column":8},"end":{"line":25,"column":16}},"2":{"start":{"line":27,"column":8},"end":{"line":27,"column":39}},"3":{"start":{"line":28,"column":8},"end":{"line":28,"column":60}},"4":{"start":{"line":29,"column":8},"end":{"line":29,"column":61}},"5":{"start":{"line":30,"column":8},"end":{"line":30,"column":62}},"6":{"start":{"line":33,"column":8},"end":{"line":33,"column":30}},"7":{"start":{"line":35,"column":8},"end":{"line":35,"column":38}},"8":{"start":{"line":36,"column":8},"end":{"line":36,"column":25}},"9":{"start":{"line":37,"column":8},"end":{"line":38,"column":32}},"10":{"start":{"line":37,"column":49},"end":{"line":37,"column":98}},"11":{"start":{"line":37,"column":78},"end":{"line":37,"column":96}},"12":{"start":{"line":38,"column":13},"end":{"line":38,"column":32}},"13":{"start":{"line":46,"column":8},"end":{"line":46,"column":46}},"14":{"start":{"line":47,"column":8},"end":{"line":47,"column":71}},"15":{"start":{"line":47,"column":36},"end":{"line":47,"column":69}},"16":{"start":{"line":48,"column":8},"end":{"line":59,"column":11}},"17":{"start":{"line":66,"column":8},"end":{"line":66,"column":50}},"18":{"start":{"line":78,"column":8},"end":{"line":82,"column":11}},"19":{"start":{"line":86,"column":8},"end":{"line":86,"column":48}},"20":{"start":{"line":90,"column":8},"end":{"line":90,"column":52}},"21":{"start":{"line":100,"column":23},"end":{"line":100,"column":35}},"22":{"start":{"line":101,"column":8},"end":{"line":107,"column":9}},"23":{"start":{"line":102,"column":12},"end":{"line":104,"column":13}},"24":{"start":{"line":103,"column":16},"end":{"line":103,"column":42}},"25":{"start":{"line":105,"column":12},"end":{"line":105,"column":43}},"26":{"start":{"line":106,"column":12},"end":{"line":106,"column":64}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":24,"column":4},"end":{"line":24,"column":5}},"loc":{"start":{"line":24,"column":38},"end":{"line":39,"column":5}},"line":24},"1":{"name":"(anonymous_1)","decl":{"start":{"line":37,"column":72},"end":{"line":37,"column":73}},"loc":{"start":{"line":37,"column":78},"end":{"line":37,"column":96}},"line":37},"2":{"name":"(anonymous_2)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":5}},"loc":{"start":{"line":44,"column":18},"end":{"line":60,"column":5}},"line":44},"3":{"name":"(anonymous_3)","decl":{"start":{"line":47,"column":25},"end":{"line":47,"column":26}},"loc":{"start":{"line":47,"column":36},"end":{"line":47,"column":69}},"line":47},"4":{"name":"(anonymous_4)","decl":{"start":{"line":65,"column":4},"end":{"line":65,"column":5}},"loc":{"start":{"line":65,"column":16},"end":{"line":67,"column":5}},"line":65},"5":{"name":"(anonymous_5)","decl":{"start":{"line":77,"column":4},"end":{"line":77,"column":5}},"loc":{"start":{"line":77,"column":37},"end":{"line":83,"column":5}},"line":77},"6":{"name":"(anonymous_6)","decl":{"start":{"line":85,"column":4},"end":{"line":85,"column":5}},"loc":{"start":{"line":85,"column":21},"end":{"line":87,"column":5}},"line":85},"7":{"name":"(anonymous_7)","decl":{"start":{"line":89,"column":4},"end":{"line":89,"column":5}},"loc":{"start":{"line":89,"column":25},"end":{"line":91,"column":5}},"line":89},"8":{"name":"(anonymous_8)","decl":{"start":{"line":99,"column":4},"end":{"line":99,"column":5}},"loc":{"start":{"line":99,"column":46},"end":{"line":108,"column":5}},"line":99}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":24},"end":{"line":24,"column":36}},"type":"default-arg","locations":[{"start":{"line":24,"column":34},"end":{"line":24,"column":36}}],"line":24},"1":{"loc":{"start":{"line":37,"column":8},"end":{"line":38,"column":32}},"type":"if","locations":[{"start":{"line":37,"column":8},"end":{"line":38,"column":32}},{"start":{"line":37,"column":8},"end":{"line":38,"column":32}}],"line":37},"2":{"loc":{"start":{"line":37,"column":12},"end":{"line":37,"column":47}},"type":"binary-expr","locations":[{"start":{"line":37,"column":12},"end":{"line":37,"column":26}},{"start":{"line":37,"column":30},"end":{"line":37,"column":47}}],"line":37},"3":{"loc":{"start":{"line":77,"column":23},"end":{"line":77,"column":35}},"type":"default-arg","locations":[{"start":{"line":77,"column":33},"end":{"line":77,"column":35}}],"line":77},"4":{"loc":{"start":{"line":101,"column":8},"end":{"line":107,"column":9}},"type":"if","locations":[{"start":{"line":101,"column":8},"end":{"line":107,"column":9}},{"start":{"line":101,"column":8},"end":{"line":107,"column":9}}],"line":101},"5":{"loc":{"start":{"line":101,"column":12},"end":{"line":101,"column":48}},"type":"binary-expr","locations":[{"start":{"line":101,"column":12},"end":{"line":101,"column":18}},{"start":{"line":101,"column":22},"end":{"line":101,"column":48}}],"line":101},"6":{"loc":{"start":{"line":102,"column":12},"end":{"line":104,"column":13}},"type":"if","locations":[{"start":{"line":102,"column":12},"end":{"line":104,"column":13}},{"start":{"line":102,"column":12},"end":{"line":104,"column":13}}],"line":102}},"s":{"0":3,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0},"b":{"0":[0],"1":[0,0],"2":[0,0],"3":[0],"4":[0,0],"5":[0,0],"6":[0,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"823c4cdf0089375a99f23a827e45e70932e41dda"} -,"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/index.js": {"path":"/home/runner/work/react-native-tcp-socket/react-native-tcp-socket/src/index.js","statementMap":{"0":{"start":{"line":25,"column":4},"end":{"line":25,"column":51}},"1":{"start":{"line":34,"column":19},"end":{"line":34,"column":52}},"2":{"start":{"line":35,"column":4},"end":{"line":35,"column":37}},"3":{"start":{"line":36,"column":4},"end":{"line":36,"column":18}},"4":{"start":{"line":47,"column":19},"end":{"line":47,"column":31}},"5":{"start":{"line":48,"column":22},"end":{"line":48,"column":52}},"6":{"start":{"line":49,"column":4},"end":{"line":49,"column":66}},"7":{"start":{"line":49,"column":33},"end":{"line":49,"column":64}},"8":{"start":{"line":50,"column":4},"end":{"line":50,"column":60}},"9":{"start":{"line":50,"column":18},"end":{"line":50,"column":60}},"10":{"start":{"line":51,"column":4},"end":{"line":51,"column":28}},"11":{"start":{"line":52,"column":4},"end":{"line":52,"column":21}},"12":{"start":{"line":61,"column":22},"end":{"line":61,"column":34}},"13":{"start":{"line":62,"column":4},"end":{"line":62,"column":48}},"14":{"start":{"line":66,"column":14},"end":{"line":66,"column":68}},"15":{"start":{"line":67,"column":14},"end":{"line":67,"column":40}},"16":{"start":{"line":68,"column":16},"end":{"line":68,"column":40}},"17":{"start":{"line":71,"column":14},"end":{"line":71,"column":36}},"18":{"start":{"line":72,"column":16},"end":{"line":83,"column":1}},"19":{"start":{"line":91,"column":4},"end":{"line":91,"column":31}},"20":{"start":{"line":100,"column":4},"end":{"line":100,"column":31}},"21":{"start":{"line":109,"column":4},"end":{"line":110,"column":37}},"22":{"start":{"line":109,"column":23},"end":{"line":109,"column":32}},"23":{"start":{"line":110,"column":9},"end":{"line":110,"column":37}},"24":{"start":{"line":110,"column":28},"end":{"line":110,"column":37}},"25":{"start":{"line":111,"column":4},"end":{"line":111,"column":13}},"26":{"start":{"line":131,"column":0},"end":{"line":145,"column":2}}},"fnMap":{"0":{"name":"createServer","decl":{"start":{"line":24,"column":9},"end":{"line":24,"column":21}},"loc":{"start":{"line":24,"column":51},"end":{"line":26,"column":1}},"line":24},"1":{"name":"createTLSServer","decl":{"start":{"line":33,"column":9},"end":{"line":33,"column":24}},"loc":{"start":{"line":33,"column":54},"end":{"line":37,"column":1}},"line":33},"2":{"name":"connectTLS","decl":{"start":{"line":46,"column":9},"end":{"line":46,"column":19}},"loc":{"start":{"line":46,"column":39},"end":{"line":53,"column":1}},"line":46},"3":{"name":"(anonymous_3)","decl":{"start":{"line":49,"column":27},"end":{"line":49,"column":28}},"loc":{"start":{"line":49,"column":33},"end":{"line":49,"column":64}},"line":49},"4":{"name":"createConnection","decl":{"start":{"line":60,"column":9},"end":{"line":60,"column":25}},"loc":{"start":{"line":60,"column":45},"end":{"line":63,"column":1}},"line":60},"5":{"name":"isIPv4","decl":{"start":{"line":90,"column":9},"end":{"line":90,"column":15}},"loc":{"start":{"line":90,"column":23},"end":{"line":92,"column":1}},"line":90},"6":{"name":"isIPv6","decl":{"start":{"line":99,"column":9},"end":{"line":99,"column":15}},"loc":{"start":{"line":99,"column":23},"end":{"line":101,"column":1}},"line":99},"7":{"name":"isIP","decl":{"start":{"line":108,"column":9},"end":{"line":108,"column":13}},"loc":{"start":{"line":108,"column":21},"end":{"line":112,"column":1}},"line":108}},"branchMap":{"0":{"loc":{"start":{"line":50,"column":4},"end":{"line":50,"column":60}},"type":"if","locations":[{"start":{"line":50,"column":4},"end":{"line":50,"column":60}},{"start":{"line":50,"column":4},"end":{"line":50,"column":60}}],"line":50},"1":{"loc":{"start":{"line":109,"column":4},"end":{"line":110,"column":37}},"type":"if","locations":[{"start":{"line":109,"column":4},"end":{"line":110,"column":37}},{"start":{"line":109,"column":4},"end":{"line":110,"column":37}}],"line":109},"2":{"loc":{"start":{"line":110,"column":9},"end":{"line":110,"column":37}},"type":"if","locations":[{"start":{"line":110,"column":9},"end":{"line":110,"column":37}},{"start":{"line":110,"column":9},"end":{"line":110,"column":37}}],"line":110}},"s":{"0":6,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":1,"13":1,"14":3,"15":3,"16":3,"17":3,"18":3,"19":5,"20":4,"21":3,"22":1,"23":2,"24":1,"25":1,"26":3},"f":{"0":6,"1":0,"2":0,"3":0,"4":1,"5":5,"6":4,"7":3},"b":{"0":[0,0],"1":[1,2],"2":[1,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"36864a6bf7898ecec25a2f1b0b492ad0281997b4"} -} diff --git a/cpp/TcpDataBridge.cpp b/cpp/TcpDataBridge.cpp new file mode 100644 index 0000000..7101478 --- /dev/null +++ b/cpp/TcpDataBridge.cpp @@ -0,0 +1,438 @@ +// VENHO fork — Phase 1 zero-copy data-plane cxxTurboModule impl. +// +// Milestone-1d finding: removing the base64 PAYLOAD was not enough — any +// per-chunk crossing of the legacy RCTDeviceEventEmitter bridge +// (invokeJavaMethod → dynamicFromValue → folly::dynamic) still OOMs, +// because the bridge's pending-call backlog on the JS thread grows +// unbounded regardless of per-call size. So BOTH the bytes AND the +// "data available" signal must avoid the legacy bridge. +// +// This file therefore also owns a JSI readable notifier: JS registers a +// single `setReadable((id)=>void)` callback; the native socket read +// thread calls TcpDataBridge::signalReadable(id) (via JNI) which hops to +// the JS thread through the CallInvoker and invokes that JS callback +// with just the id. No folly::dynamic, no event emitter, no per-chunk +// Java→JS marshalling. Bytes are then pulled zero-copy via read(id). + +#include "TcpDataBridge.h" + +#include "TcpInboundRegistry.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +// VENHO Phase 1 — pull TcpBridgeJni.cpp.o into the merged libappmodules +// .so. That TU's only symbol (the implicitly-bound nativeInstallBridge +// JNI entry) is referenced solely at RUNTIME by name, so the NDK +// --gc-sections link would dead-strip the whole object — leaving +// implicit JNI binding with nothing to resolve. This TU is kept (the +// codegen cxxTurboModule depends on it), so a single hard link-time +// reference from here force-keeps the JNI object. Defined in +// android/TcpBridgeJni.cpp. +extern "C" void venho_tcpBridgeJniKeepAnchor(); + +namespace facebook::react { + +namespace { + +// Process-global JSI notifier state. The native read thread is NOT a JS +// thread; it must marshal onto the JS thread via the CallInvoker before +// touching the runtime or the JS callback. +// +// VENHO Phase 1 — readable-signal COALESCING (Scenario-C OOM #3 fix). +// The native read loop calls signalReadable() once per in.read() chunk +// (≤16KB). Each call previously did an unconditional +// CallInvoker::invokeAsync(), scheduling a fresh lambda onto RN 0.83's +// bridgeless RuntimeScheduler — whose per-task bookkeeping is a +// folly::dynamic. On an active libp2p connection the native thread +// signals FAR faster than the JS thread drains, so that queue grew +// unbounded: ~9.15M un-freed folly::dynamic nodes / classes 144–448B in +// ~5s, ending in a recursive folly::dynamic::ObjectImpl teardown stack +// overflow (proven 2026-05-17 with kad-DHT OFF + maxConnections:3, i.e. +// ONE idle connection — refuting connection-churn AND crypto, isolating +// THIS path). The JS drain (_drainInbound) already empties the WHOLE +// per-socket queue per call, so at most ONE signal need be in flight per +// socket: `pending` is set test-and-set before scheduling and cleared +// inside the scheduled task right before the JS call, so bytes arriving +// during/after a drain re-arm exactly one more signal. Millions of +// invokeAsync collapse to ~one per drain cycle — no data lost (a +// coalesced-away signal's bytes are taken by the in-flight drain). +struct ReadableNotifier { + std::mutex mutex; + jsi::Runtime* runtime = nullptr; + std::shared_ptr invoker; + std::shared_ptr jsCallback; // (id:number)=>void + // socket id -> whether a readable invokeAsync is already in flight for + // it (guarded by `mutex`). Bounded by the number of live sockets. + std::unordered_map pending; +}; + +ReadableNotifier& notifier() { + static ReadableNotifier n; + return n; +} + +// Symmetric notifier for the OUTBOUND write-drain signal. Fired from the +// native socket WRITE thread (also not a JS thread) when the registry's +// per-socket outbound queue drains below its low watermark, so Socket.js +// can emit `drain`. Same CallInvoker hop discipline as the readable one. +struct WriteDrainNotifier { + std::mutex mutex; + jsi::Runtime* runtime = nullptr; + std::shared_ptr invoker; + std::shared_ptr jsCallback; // (id:number)=>void +}; + +WriteDrainNotifier& writeDrainNotifier() { + static WriteDrainNotifier n; + return n; +} + +// Notifier for the per-write `written` ACK. Fired from the native socket +// WRITE thread (TcpSenderTask, via JNI) after each chunk is flushed to +// the OutputStream. This REPLACES TcpEventListener.onWritten → +// RCTDeviceEventEmitter.emit("written",…): that legacy-bridge emit ran +// once PER WRITE and, under libp2p handshake volume, accumulated an +// unbounded nested folly::dynamic in the bridgeless event-emitter queue +// — the Scenario-C OOM #2 (giant recursive folly::dynamic teardown, +// same class of bug milestone-1d hit on the readable side). Same +// CallInvoker hop discipline; the JS callback gets (id,msgId,err) where +// err is "" on success. +struct WrittenNotifier { + std::mutex mutex; + jsi::Runtime* runtime = nullptr; + std::shared_ptr invoker; + // (id:number, msgId:number, err:string)=>void (err==="" ⇒ success) + std::shared_ptr jsCallback; +}; + +WrittenNotifier& writtenNotifier() { + static WrittenNotifier n; + return n; +} + +// jsi::MutableBuffer that OWNS a moved InboundChunk. The JS ArrayBuffer +// returned by read() is constructed over this — its storage IS the +// received socket bytes (zero copy). When Hermes GCs the ArrayBuffer, +// this destructs and the chunk's vector frees. Bounded because the +// registry's per-socket queue is bounded (= the backpressure). +class ChunkBuffer : public jsi::MutableBuffer { + public: + explicit ChunkBuffer(venho::InboundChunk&& c) : chunk_(std::move(c)) {} + size_t size() const override { return chunk_.bytes.size(); } + uint8_t* data() override { return chunk_.bytes.data(); } + + private: + venho::InboundChunk chunk_; +}; + +// The installed global.__TcpDataBridge host object. +class DataBridgeHostObject : public jsi::HostObject { + public: + jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override { + auto prop = name.utf8(rt); + if (prop == "read") { + // read(id: number): ArrayBuffer | null — zero-copy pop. + return jsi::Function::createFromHostFunction( + rt, jsi::PropNameID::forAscii(rt, "read"), 1, + [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 1 || !args[0].isNumber()) { + return jsi::Value::null(); + } + auto id = static_cast(args[0].asNumber()); + venho::InboundChunk chunk; + if (!venho::TcpInboundRegistry::instance().popInbound(id, + chunk)) { + return jsi::Value::null(); + } + auto buf = std::make_shared(std::move(chunk)); + return jsi::ArrayBuffer(rt, buf); + }); + } + if (prop == "setReadable") { + // setReadable((id:number)=>void): register the single JS readable + // notifier. Stored with the runtime + CallInvoker so the native + // read thread can signal without ever touching the legacy bridge. + return jsi::Function::createFromHostFunction( + rt, jsi::PropNameID::forAscii(rt, "setReadable"), 1, + [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 1 || !args[0].isObject()) { + return jsi::Value::undefined(); + } + auto fn = args[0].asObject(rt).asFunction(rt); + auto& n = notifier(); + std::lock_guard lk(n.mutex); + n.runtime = &rt; + n.jsCallback = + std::make_shared(std::move(fn)); + return jsi::Value::undefined(); + }); + } + if (prop == "write") { + // write(id: number, data: ArrayBuffer, msgId: number): boolean + // Zero-(legacy-)copy outbound. The bytes are copied ONCE off the + // JS ArrayBuffer into the C++ registry (the JS heap can't be held + // past this host call) — NO base64, NO @ReactMethod, NO + // jsi::dynamicFromValue. Returns false at the outbound high + // watermark so Socket.js latches writableNeedDrain. + return jsi::Function::createFromHostFunction( + rt, jsi::PropNameID::forAscii(rt, "write"), 3, + [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 3 || !args[0].isNumber() || !args[1].isObject() || + !args[2].isNumber()) { + return jsi::Value(true); + } + auto obj = args[1].asObject(rt); + if (!obj.isArrayBuffer(rt)) { + return jsi::Value(true); + } + auto id = static_cast(args[0].asNumber()); + auto msgId = static_cast(args[2].asNumber()); + auto ab = obj.getArrayBuffer(rt); + bool keepWriting = + venho::TcpInboundRegistry::instance().pushOutbound( + id, ab.data(rt), ab.size(rt), msgId); + return jsi::Value(keepWriting); + }); + } + if (prop == "setWriteDrainable") { + // setWriteDrainable((id:number)=>void): register the single JS + // write-drain notifier (symmetric to setReadable). Fired when the + // native write thread drains the outbound queue below its low + // watermark so Socket.js can emit `drain`. + return jsi::Function::createFromHostFunction( + rt, jsi::PropNameID::forAscii(rt, "setWriteDrainable"), 1, + [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 1 || !args[0].isObject()) { + return jsi::Value::undefined(); + } + auto fn = args[0].asObject(rt).asFunction(rt); + auto& n = writeDrainNotifier(); + std::lock_guard lk(n.mutex); + n.runtime = &rt; + n.jsCallback = + std::make_shared(std::move(fn)); + return jsi::Value::undefined(); + }); + } + if (prop == "setWritten") { + // setWritten((id:number, msgId:number, err:string)=>void): + // register the single JS per-write ACK callback (symmetric to + // setReadable). Replaces the legacy `written` device event — the + // native write thread calls signalWritten() after each flush; + // err==="" means success. + return jsi::Function::createFromHostFunction( + rt, jsi::PropNameID::forAscii(rt, "setWritten"), 1, + [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 1 || !args[0].isObject()) { + return jsi::Value::undefined(); + } + auto fn = args[0].asObject(rt).asFunction(rt); + auto& n = writtenNotifier(); + std::lock_guard lk(n.mutex); + n.runtime = &rt; + n.jsCallback = + std::make_shared(std::move(fn)); + return jsi::Value::undefined(); + }); + } + return jsi::Value::undefined(); + } +}; + +} // namespace + +TcpDataBridge::TcpDataBridge(std::shared_ptr jsInvoker) + : NativeTcpDataBridgeCxxSpec(jsInvoker) { + // Stash the CallInvoker so signalReadable() / signalWriteDrain() + // (called from the native socket read/write threads via JNI) can hop + // onto the JS thread. Both directions share the one invoker. + { + auto& n = notifier(); + std::lock_guard lk(n.mutex); + n.invoker = jsInvoker; + } + { + auto& wn = writeDrainNotifier(); + std::lock_guard lk(wn.mutex); + wn.invoker = jsInvoker; + } + { + auto& wrn = writtenNotifier(); + std::lock_guard lk(wrn.mutex); + wrn.invoker = std::move(jsInvoker); + } + // Install the registry → JS write-drain hook. The registry calls this + // C-function ptr from the native write thread on the low-watermark + // crossing; it bounces to JS exactly like signalReadable. + venho::TcpInboundRegistry::instance().setWriteDrainNotifier( + &TcpDataBridge::signalWriteDrain); + + // Link-time KEEP edge (see comment at the extern decl above): a + // volatile sink defeats dead-code elimination so the reference — and + // thus TcpBridgeJni.cpp.o with the nativeInstallBridge JNI export — + // survives into the merged .so. Never actually skipped at runtime; + // the volatile read just blocks the optimiser from proving it dead. + static void (*volatile keep)() = &venho_tcpBridgeJniKeepAnchor; + keep(); +} + +bool TcpDataBridge::install(jsi::Runtime& rt) { + // rt.global() returns a temporary jsi::Object — bind by value. + auto global = rt.global(); + if (global.hasProperty(rt, "__TcpDataBridge")) { + return true; // idempotent + } + { + auto& n = notifier(); + std::lock_guard lk(n.mutex); + n.runtime = &rt; + } + { + auto& wn = writeDrainNotifier(); + std::lock_guard lk(wn.mutex); + wn.runtime = &rt; + } + { + auto& wrn = writtenNotifier(); + std::lock_guard lk(wrn.mutex); + wrn.runtime = &rt; + } + auto host = std::make_shared(); + global.setProperty(rt, "__TcpDataBridge", + jsi::Object::createFromHostObject(rt, host)); + return true; +} + +// Called from the native socket read thread (JNI). Coalesced + bounced +// to the JS thread via the CallInvoker; invokes the single JS readable +// callback with the socket id. NEVER the legacy event bridge. +void TcpDataBridge::signalReadable(int32_t id) { + std::shared_ptr invoker; + { + auto& n = notifier(); + std::lock_guard lk(n.mutex); + if (!n.invoker || !n.jsCallback || !n.runtime) { + return; // JS not ready yet — bytes stay queued; a later signal + // (or the JS resume drain) will pick them up. + } + // COALESCE: if a readable task is already in flight for this socket, + // do NOT schedule another. The pending drain empties the entire + // per-socket queue, so it will also take whatever bytes this call + // just made available. This collapses the per-16KB-chunk invokeAsync + // storm (the Scenario-C OOM #3 unbounded RuntimeScheduler/ + // folly::dynamic backlog) to ~one task per drain cycle. Test-and-set + // under the same mutex that the task clears the flag under. + bool& pend = n.pending[id]; + if (pend) { + return; + } + pend = true; + invoker = n.invoker; + } + invoker->invokeAsync([id]() { + auto& n = notifier(); + jsi::Runtime* rt; + std::shared_ptr cb; + { + std::lock_guard lk(n.mutex); + // Clear BEFORE the JS call: any signalReadable() racing in while + // the JS drain runs will then re-arm exactly one fresh task, so + // bytes that arrive mid-drain are never stranded. + n.pending[id] = false; + rt = n.runtime; + cb = n.jsCallback; + } + if (rt && cb) { + cb->call(*rt, jsi::Value(static_cast(id))); + } + }); +} + +void TcpDataBridge::forgetSocket(int32_t id) { + auto& n = notifier(); + std::lock_guard lk(n.mutex); + n.pending.erase(id); +} + +// Called from the native socket WRITE thread (via the registry's +// write-drain hook) when the per-socket outbound queue drained below its +// low watermark. Symmetric to signalReadable: bounce to the JS thread +// via the CallInvoker and invoke the JS write-drain callback with the +// socket id so Socket.js can emit `drain`. NEVER the legacy bridge. +void TcpDataBridge::signalWriteDrain(int32_t id) { + std::shared_ptr invoker; + { + auto& n = writeDrainNotifier(); + std::lock_guard lk(n.mutex); + if (!n.invoker || !n.jsCallback || !n.runtime) { + return; // JS not ready — the next pushOutbound/drain re-signals. + } + invoker = n.invoker; + } + invoker->invokeAsync([id]() { + auto& n = writeDrainNotifier(); + jsi::Runtime* rt; + std::shared_ptr cb; + { + std::lock_guard lk(n.mutex); + rt = n.runtime; + cb = n.jsCallback; + } + if (rt && cb) { + cb->call(*rt, jsi::Value(static_cast(id))); + } + }); +} + +// Called from the native socket WRITE thread (TcpSenderTask, via JNI) +// after each chunk is flushed. Bounces to the JS thread via the +// CallInvoker and invokes the JS `written` ACK callback with +// (id, msgId, err) — err=="" on success. This is the per-write ACK +// REPLACEMENT for the legacy RCTDeviceEventEmitter "written" event, +// which accumulated an unbounded folly::dynamic per write (Scenario-C +// OOM #2). `err` is copied into the lambda (the JNI string is gone by +// the time the async hop runs). +void TcpDataBridge::signalWritten(int32_t id, int32_t msgId, + const std::string& err) { + std::shared_ptr invoker; + { + auto& n = writtenNotifier(); + std::lock_guard lk(n.mutex); + if (!n.invoker || !n.jsCallback || !n.runtime) { + return; // JS not ready — the write still happened; the optional + // user write-callback just won't fire (best-effort ACK, + // exactly as a dropped legacy event would have been). + } + invoker = n.invoker; + } + invoker->invokeAsync([id, msgId, err]() { + auto& n = writtenNotifier(); + jsi::Runtime* rt; + std::shared_ptr cb; + { + std::lock_guard lk(n.mutex); + rt = n.runtime; + cb = n.jsCallback; + } + if (rt && cb) { + cb->call(*rt, jsi::Value(static_cast(id)), + jsi::Value(static_cast(msgId)), + jsi::String::createFromUtf8(*rt, err)); + } + }); +} + +} // namespace facebook::react diff --git a/cpp/TcpDataBridge.h b/cpp/TcpDataBridge.h new file mode 100644 index 0000000..d77afb9 --- /dev/null +++ b/cpp/TcpDataBridge.h @@ -0,0 +1,76 @@ +// VENHO fork — Phase 1 zero-copy data-plane cxxTurboModule. +// +// Header name `TcpDataBridge` matches react-native.config.js +// `cxxModuleHeaderName`. Extends the codegen-generated CxxSpec base for +// the `NativeTcpDataBridge` TS spec (one method: install()). install() +// registers `global.__TcpDataBridge`, a jsi::HostObject exposing the +// FULL zero-copy data plane (no codegen/folly path — that was the +// Phase-0 OOM, then the residual Scenario-C OOM on the write side): +// +// read(id) -> next inbound chunk as a ZERO-COPY +// jsi::ArrayBuffer (MutableBuffer owns the +// moved bytes; freeing it frees the chunk). +// setReadable(cb) -> register the single inbound-ready JS cb. +// write(id, ab, msgId)-> enqueue an outbound chunk, copied once off +// the JS ArrayBuffer into the C++ registry; +// returns false at the outbound high watermark +// (JS must stop writing). NO base64, NO +// @ReactMethod, NO jsi::dynamicFromValue. +// setWriteDrainable(cb)-> register the single write-drain JS cb, +// fired when the native write thread drains the +// outbound queue below its low watermark. + +#pragma once + +// Codegen emits the CxxSpec base (NativeTcpDataBridgeCxxSpec) into this +// header, named after codegenConfig.name. On the include path via the +// generated jni/CMakeLists.txt target_include_directories. +#include + +#include +#include + +namespace facebook::react { + +class TcpDataBridge : public NativeTcpDataBridgeCxxSpec { + public: + explicit TcpDataBridge(std::shared_ptr jsInvoker); + + // Codegen method: install the global JSI host object. Idempotent. + bool install(jsi::Runtime& rt); + + // Called from the native socket read thread (via JNI) after bytes are + // queued. Hops to the JS thread through the CallInvoker and invokes + // the registered JS readable callback with the socket id. Static: no + // module instance is needed on the read thread (process-global + // notifier state). NEVER crosses the legacy event bridge — that was + // the milestone-1d OOM. + static void signalReadable(int32_t id); + + // Registry write-drain notifier (C-function ptr signature). The C++ + // registry calls this from the native write thread when the outbound + // queue drains below its low watermark; it hops to the JS thread via + // the CallInvoker and invokes the registered JS write-drain callback + // with the socket id (so Socket.js can emit `drain`). Symmetric to + // signalReadable; installed into the registry by the constructor. + static void signalWriteDrain(int32_t id); + + // Called from the native socket WRITE thread (TcpSenderTask, via JNI) + // after each chunk is flushed to the OutputStream. Hops to the JS + // thread via the CallInvoker and invokes the registered JS `written` + // ACK callback with (id, msgId, err) — err=="" on success. This is + // the per-write ACK replacement for the legacy RCTDeviceEventEmitter + // "written" event (the Scenario-C OOM #2: per-write folly::dynamic + // accumulation in the bridgeless event-emitter queue). + static void signalWritten(int32_t id, int32_t msgId, + const std::string& err); + + // Called from nativeUnregisterSocket (JNI) when a socket is torn down. + // Erases that socket's readable-coalescing `pending` entry so the + // process-global map does not grow across many short-lived connections + // (libp2p opens/closes lots of peer connections). Static: same + // process-global notifier state as signalReadable. + static void forgetSocket(int32_t id); +}; + +} // namespace facebook::react diff --git a/cpp/TcpInboundRegistry.cpp b/cpp/TcpInboundRegistry.cpp new file mode 100644 index 0000000..f3fc6cc --- /dev/null +++ b/cpp/TcpInboundRegistry.cpp @@ -0,0 +1,184 @@ +// VENHO fork — Phase 1 zero-copy inbound registry implementation. + +#include "TcpInboundRegistry.h" + +namespace venho { + +TcpInboundRegistry& TcpInboundRegistry::instance() { + static TcpInboundRegistry inst; + return inst; +} + +void TcpInboundRegistry::registerSocket(int32_t id) { + std::lock_guard lk(mutex_); + sockets_[id] = std::make_unique(); +} + +void TcpInboundRegistry::unregisterSocket(int32_t id) { + { + std::lock_guard lk(mutex_); + sockets_.erase(id); + } + // Wake any native write thread blocked in waitPopOutbound for this + // (now-erased) socket so it observes the removal and exits. + outCv_.notify_all(); +} + +bool TcpInboundRegistry::pushInbound(int32_t id, const uint8_t* data, + size_t len) { + std::lock_guard lk(mutex_); + auto it = sockets_.find(id); + if (it == sockets_.end()) { + // Socket gone (closed/destroyed). Drop — the read loop will exit. + return true; + } + SocketInbound* s = it->second.get(); + InboundChunk chunk; + chunk.bytes.assign(data, data + len); + s->queue.push_back(std::move(chunk)); + if (s->queue.size() >= SocketInbound::kHighWater) { + // High watermark: tell the caller to pause the native read loop. + // popInbound() will fire resume() once drained to low watermark. + s->paused = true; + return false; + } + return true; +} + +bool TcpInboundRegistry::popInbound(int32_t id, InboundChunk& out) { + std::lock_guard lk(mutex_); + auto it = sockets_.find(id); + if (it == sockets_.end()) { + return false; + } + SocketInbound* s = it->second.get(); + if (s->queue.empty()) { + return false; + } + out = std::move(s->queue.front()); + s->queue.pop_front(); + if (s->paused && s->queue.size() <= SocketInbound::kLowWater) { + // Cleared latch → next canResume() poll lets the Java loop resume. + s->paused = false; + } + return true; +} + +bool TcpInboundRegistry::canResume(int32_t id) { + std::lock_guard lk(mutex_); + auto it = sockets_.find(id); + if (it == sockets_.end()) { + return true; // socket gone — let the loop fall through and exit + } + return !it->second->paused; +} + +// ---- OUTBOUND (JS write → native socket OutputStream) ----------------- + +void TcpInboundRegistry::setWriteDrainNotifier(WriteDrainFn fn) { + std::lock_guard lk(mutex_); + writeDrainFn_ = fn; +} + +bool TcpInboundRegistry::pushOutbound(int32_t id, const uint8_t* data, + size_t len, int32_t msgId) { + bool keepWriting; + { + std::lock_guard lk(mutex_); + auto it = sockets_.find(id); + if (it == sockets_.end()) { + // Socket gone — drop. Returning true keeps the JS caller from + // spuriously latching writableNeedDrain on a dead socket; the + // write/close path will surface the real error. + return true; + } + SocketInbound* s = it->second.get(); + OutboundChunk chunk; + chunk.bytes.assign(data, data + len); + chunk.msgId = msgId; + s->outQueue.push_back(std::move(chunk)); + if (s->outQueue.size() >= SocketInbound::kOutHighWater) { + // High watermark: JS must stop writing (Socket.js latches + // writableNeedDrain). popOutbound fires the drain notifier once + // the write thread drains below the low watermark. + s->writePaused = true; + keepWriting = false; + } else { + keepWriting = true; + } + } + // Wake the native write thread (it blocks in waitPopOutbound when its + // queue is empty). Notify outside the lock. + outCv_.notify_all(); + return keepWriting; +} + +bool TcpInboundRegistry::popOutbound(int32_t id, OutboundChunk& out) { + WriteDrainFn drainFn = nullptr; + bool fireDrain = false; + { + std::lock_guard lk(mutex_); + auto it = sockets_.find(id); + if (it == sockets_.end()) { + return false; + } + SocketInbound* s = it->second.get(); + if (s->outQueue.empty()) { + return false; + } + out = std::move(s->outQueue.front()); + s->outQueue.pop_front(); + if (s->writePaused && s->outQueue.size() <= SocketInbound::kOutLowWater) { + s->writePaused = false; + if (writeDrainFn_) { + drainFn = writeDrainFn_; + fireDrain = true; + } + } + } + if (fireDrain && drainFn) { + // Notifier hops to the JS thread itself (TcpDataBridge); call it + // outside the registry lock to avoid holding it across the + // CallInvoker dispatch. + drainFn(id); + } + return true; +} + +bool TcpInboundRegistry::waitPopOutbound(int32_t id, OutboundChunk& out) { + WriteDrainFn drainFn = nullptr; + bool fireDrain = false; + { + std::unique_lock lk(mutex_); + for (;;) { + auto it = sockets_.find(id); + if (it == sockets_.end()) { + return false; // socket unregistered — write thread must exit + } + SocketInbound* s = it->second.get(); + if (!s->outQueue.empty()) { + out = std::move(s->outQueue.front()); + s->outQueue.pop_front(); + if (s->writePaused && + s->outQueue.size() <= SocketInbound::kOutLowWater) { + s->writePaused = false; + if (writeDrainFn_) { + drainFn = writeDrainFn_; + fireDrain = true; + } + } + break; + } + // Empty: block until pushOutbound / unregisterSocket notifies. + // Predicate re-checked each wake (handles spurious wakeups and the + // unregister-while-waiting race). + outCv_.wait(lk); + } + } + if (fireDrain && drainFn) { + drainFn(id); + } + return true; +} + +} // namespace venho diff --git a/cpp/TcpInboundRegistry.h b/cpp/TcpInboundRegistry.h new file mode 100644 index 0000000..8197929 --- /dev/null +++ b/cpp/TcpInboundRegistry.h @@ -0,0 +1,146 @@ +// VENHO fork — Phase 1 zero-copy socket data registry (platform-shared +// C++). Owns BOTH directions of the socket data plane: +// +// INBOUND — the native socket read thread (Android: TcpReceiverTask +// via JNI; iOS: GCDAsyncSocket via the same C++ API) pushes received +// chunks here as owned byte buffers. JS pulls them via the JSI host +// object as zero-copy ArrayBuffers (the buffer's storage IS the pushed +// chunk; freeing the JS ArrayBuffer frees the chunk). +// +// OUTBOUND — JS pushes bytes to send via the JSI host object's write(), +// straight from the JS ArrayBuffer into an owned chunk (one copy off +// the JS heap; NO base64, NO folly::dynamic, NO @ReactMethod). The +// native socket WRITE thread pops chunks here and writes them to the +// socket OutputStream. This is the symmetric replacement for the +// Phase-0 `NativeModules.TcpSockets.write(id, base64, msgId)` Java +// TurboModule call, whose per-write jsi::dynamicFromValue marshalling +// was the residual Scudo OOM (Scenario C, after inbound was fixed). +// +// Backpressure (both directions): each per-socket queue has a bounded +// depth. Inbound: pushInbound() returns false at the high watermark and +// the native read loop must pause — TCP flow control backpressures the +// peer; the loop polls canResume(). Outbound: pushOutbound() returns +// false at the high watermark and JS must stop writing (Socket.js +// surfaces this as `writableNeedDrain`); the C++ side fires the drain +// notifier once the write thread has drained below the low watermark. +// Memory is bounded by the fixed queues, never the unbounded +// folly::dynamic backlog. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace venho { + +// One chunk of socket data; owns its bytes. INBOUND chunks are moved +// into the queue then handed to JS as the backing store of a +// jsi::ArrayBuffer (no copy). OUTBOUND chunks are copied once off the JS +// ArrayBuffer (the JS heap can't be referenced past the host call) then +// moved to the native write thread. `msgId` is meaningful only for +// outbound — it is echoed back on the `written` ack. +struct InboundChunk { + std::vector bytes; + int32_t msgId = -1; +}; + +// Outbound is structurally identical to a received chunk; alias so the +// queue/registry code reads symmetrically. +using OutboundChunk = InboundChunk; + +// Bounded per-socket queues (both directions) + watermark state. +class SocketInbound { + public: + // High/low watermarks in queued chunks. 16 KiB native reads × 64 = + // ~1 MiB ceiling per socket before the read loop is paused — far + // below anything that pressures the allocator, and the peer's TCP + // window stalls once we stop reading. + static constexpr size_t kHighWater = 64; + static constexpr size_t kLowWater = 16; + + // Outbound watermarks. libp2p/yamux emits many small frames; a deeper + // queue absorbs bursts without forcing JS to spin on drain, while + // still bounding memory (256 × typical frame ≪ allocator pressure). + static constexpr size_t kOutHighWater = 256; + static constexpr size_t kOutLowWater = 64; + + // High-watermark latch. Set true by pushInbound when the queue fills + // (Java read loop must pause); cleared by popInbound once drained to + // the low watermark (Java polls canResume to learn this). + bool paused = false; + std::deque queue; + + // Outbound: set true by pushOutbound at the high watermark (JS must + // stop writing); cleared by popOutbound at the low watermark, which + // also fires the drain notifier so JS resumes. + bool writePaused = false; + std::deque outQueue; +}; + +class TcpInboundRegistry { + public: + static TcpInboundRegistry& instance(); + + // Called by the native socket layer when a socket starts listening. + void registerSocket(int32_t id); + void unregisterSocket(int32_t id); + + // Native read thread → push a received chunk (takes ownership). + // Returns true if the caller may keep reading; false if the queue hit + // the high watermark and the caller must pause the socket read loop + // (the registry will call the resume callback when drained). + bool pushInbound(int32_t id, const uint8_t* data, size_t len); + + // JS (via the JSI host object) → pop the next chunk for a socket. + // Returns false if the queue is empty. On the drain that crosses the + // low watermark while paused, schedules the resume callback. + bool popInbound(int32_t id, InboundChunk& out); + + // Java's paused read loop polls this: true once the queue drained to + // the low watermark (so the loop may resume reading the socket). + // Poll-based instead of a JNI upcall to keep the glue minimal and + // avoid attaching the C++ pop-caller thread to the JVM. + bool canResume(int32_t id); + + // ---- OUTBOUND (JS write → native socket OutputStream) ---------------- + + // JS (via the JSI host object's write()) → enqueue bytes to send, + // copied once off the JS ArrayBuffer (the JS heap can't be held past + // the host call). `msgId` is echoed on the write ack. Returns true if + // JS may keep writing; false if the bounded outbound queue hit the + // high watermark and JS must stop (Socket.js sets writableNeedDrain). + bool pushOutbound(int32_t id, const uint8_t* data, size_t len, + int32_t msgId); + + // Native socket WRITE thread → pop the next chunk to write to the + // OutputStream. Returns false if the queue is empty. On the pop that + // crosses the low watermark while write-paused, clears the latch and + // (if set) fires the JS write-drain notifier so JS resumes writing. + bool popOutbound(int32_t id, OutboundChunk& out); + + // The native write thread blocks here when its outbound queue is + // empty, woken by pushOutbound — avoids a busy-poll on the write + // executor. Returns false if the socket was unregistered (thread must + // exit). `out` receives the next chunk when available. + bool waitPopOutbound(int32_t id, OutboundChunk& out); + + // Register the JS write-drain notifier (one global callback, keyed by + // id at call time) — symmetric to the inbound readable notifier. Set + // by TcpDataBridge::setWriteDrainNotifier; invoked by popOutbound on + // the low-watermark crossing so JS can emit `drain`. + using WriteDrainFn = void (*)(int32_t id); + void setWriteDrainNotifier(WriteDrainFn fn); + + private: + std::mutex mutex_; + std::condition_variable outCv_; + WriteDrainFn writeDrainFn_ = nullptr; + std::unordered_map> sockets_; +}; + +} // namespace venho diff --git a/package.json b/package.json index 6085359..2d96c19 100644 --- a/package.json +++ b/package.json @@ -77,5 +77,18 @@ "dependencies": { "buffer": "^5.4.3", "eventemitter3": "^4.0.7" + }, + "codegenConfig": { + "name": "RNTcpSocketsSpec", + "type": "modules", + "jsSrcsDir": "src", + "outputDir": { + "ios": "ios/generated", + "android": "android/generated" + }, + "android": { + "javaPackageName": "com.asterinet.react.tcpsocket" + }, + "includesGeneratedCode": true } } diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..ce3da53 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,23 @@ +/** + * VENHO fork — Phase 1 JSI data-path. + * + * The control plane stays the legacy `TcpSockets` NativeModule + * (connect/write/listen/etc, autolinked normally). This block adds the + * C++ cxxTurboModule that installs the zero-copy `TcpDataBridge` JSI + * host object (read path off the legacy folly::dynamic bridge). Modeled + * exactly on react-native-quick-base64's working RN-0.83 New-Arch wiring. + * + * @type {import('@react-native-community/cli-types').UserDependencyConfig} + */ +module.exports = { + dependency: { + platforms: { + android: { + cmakeListsPath: 'generated/jni/CMakeLists.txt', + cxxModuleCMakeListsModuleName: 'react-native-tcp-socket', + cxxModuleCMakeListsPath: 'CMakeLists.txt', + cxxModuleHeaderName: 'TcpDataBridge', + }, + }, + }, +}; diff --git a/src/Globals.js b/src/Globals.js index b12410b..6e7375f 100644 --- a/src/Globals.js +++ b/src/Globals.js @@ -9,4 +9,164 @@ function getNextId() { const nativeEventEmitter = new NativeEventEmitter(Sockets); -export { nativeEventEmitter, getNextId }; +/** + * VENHO fork — Phase 1 zero-copy inbound data path. + * + * Installs `global.__TcpDataBridge` (a JSI host object) once, via the + * RNTcpDataBridge cxxTurboModule. Inbound socket bytes are pulled from + * it as zero-copy ArrayBuffers (`read(id)`), and the "data available" + * signal arrives through a single JSI callback (`setReadable`) hopped + * onto the JS thread by the C++ CallInvoker — NOT the legacy + * RCTDeviceEventEmitter. Milestone 1d proved ANY per-chunk legacy-bridge + * crossing OOMs (invokeJavaMethod → folly::dynamic backlog), even a tiny + * {id} payload — so both the bytes and the signal stay off that bridge. + * + * The Jest env mocks this whole module, so the native bits are absent + * there; Socket.js degrades to a no-op drain and the regression suite is + * unaffected. + */ +let _dataBridge; +let _dataBridgeTried = false; + +// socket id -> () => void (the per-Socket inbound drain handler) +const _readableHandlers = new Map(); + +// socket id -> () => void (the per-Socket outbound write-drain handler; +// fired when the native write loop drained the C++ outbound queue below +// its low watermark — symmetric to the readable handler). +const _writeDrainHandlers = new Map(); + +// socket id -> (msgId:number, err:string) => void (the per-Socket +// write-ACK handler; fired once per write by the native write loop. +// Replaces the legacy `written` RCTDeviceEventEmitter event — that +// per-write emit accumulated an unbounded folly::dynamic, Scenario-C +// OOM #2). err === '' means success. +const _writtenHandlers = new Map(); + +// "callback already installed" guards. These MUST live in module JS +// state, NOT as properties on the bridge: `bridge` is a JSI HostObject +// with a C++ default setter, and Hermes throws +// `TypeError: Cannot assign to property '' on HostObject with +// default setter` for any arbitrary-property write on it. Stashing the +// guard flag on the host object (the original approach) threw inside +// getTcpDataBridge()'s try, was swallowed → _dataBridge=null → every +// libp2p noise write failed → dials abandoned → conns=0 (root cause, +// device-proven 2026-05-17). `__TcpDataBridge` is a process-global +// singleton, so module-level booleans are the exact equivalent of the +// per-bridge flags with no behavior change. +let _readableInstalled = false; +let _writeDrainableInstalled = false; +let _writtenInstalled = false; + +function _ensureReadableInstalled(bridge) { + if (_readableInstalled) return; + _readableInstalled = true; + // ONE JS callback for all sockets; C++ invokes it (via CallInvoker) + // with the socket id whenever bytes were queued for that id. + bridge.setReadable((id) => { + const h = _readableHandlers.get(id); + if (h) h(); + }); +} + +function _ensureWriteDrainableInstalled(bridge) { + if (_writeDrainableInstalled) return; + if (typeof bridge.setWriteDrainable !== 'function') return; + _writeDrainableInstalled = true; + // ONE JS callback for all sockets; C++ invokes it (via CallInvoker) + // with the socket id when that socket's outbound queue drained below + // the low watermark, so Socket.js can clear writableNeedDrain/emit + // 'drain'. + bridge.setWriteDrainable((id) => { + const h = _writeDrainHandlers.get(id); + if (h) h(); + }); +} + +function _ensureWrittenInstalled(bridge) { + if (_writtenInstalled) return; + if (typeof bridge.setWritten !== 'function') return; + _writtenInstalled = true; + // ONE JS callback for all sockets; C++ invokes it (via CallInvoker) + // once per write with (id, msgId, err) — err === '' on success. + bridge.setWritten((id, msgId, err) => { + const h = _writtenHandlers.get(id); + if (h) h(msgId, err); + }); +} + +function getTcpDataBridge() { + if (_dataBridgeTried) return _dataBridge || null; + _dataBridgeTried = true; + try { + // Lazy require: the spec calls TurboModuleRegistry.getEnforcing, + // which throws if the native module isn't present. Never let + // that crash the JS app — degrade to a no-op (Jest / unexpected). + const mod = require('./NativeTcpDataBridge').default; + if (mod && typeof mod.install === 'function') { + mod.install(); + const g = typeof global !== 'undefined' ? global : /* istanbul ignore next */ undefined; + _dataBridge = g && g.__TcpDataBridge ? g.__TcpDataBridge : null; + if (_dataBridge && typeof _dataBridge.setReadable === 'function') { + _ensureReadableInstalled(_dataBridge); + } + if (_dataBridge && typeof _dataBridge.setWriteDrainable === 'function') { + _ensureWriteDrainableInstalled(_dataBridge); + } + if (_dataBridge && typeof _dataBridge.setWritten === 'function') { + _ensureWrittenInstalled(_dataBridge); + } + } + } catch (e) { + // A wiring failure here is fatal to the data path (every socket + // write throws downstream). Do NOT silently swallow — surface it + // loudly so it can never again masquerade as a mystery conns=0. + // The Jest env mocks this whole module, so this path is device-only. + _dataBridge = null; + // eslint-disable-next-line no-console + console.error( + '[react-native-tcp-socket] TcpDataBridge install failed: ' + + String(e && e.message ? e.message : e) + ); + } + return _dataBridge || null; +} + +/** Register a socket's inbound drain handler (called on its id signal). */ +function registerReadableHandler(id, handler) { + _readableHandlers.set(id, handler); +} + +function unregisterReadableHandler(id) { + _readableHandlers.delete(id); +} + +/** Register a socket's outbound write-drain handler (id signal). */ +function registerWriteDrainHandler(id, handler) { + _writeDrainHandlers.set(id, handler); +} + +function unregisterWriteDrainHandler(id) { + _writeDrainHandlers.delete(id); +} + +/** Register a socket's per-write ACK handler: (msgId, err) => void. */ +function registerWrittenHandler(id, handler) { + _writtenHandlers.set(id, handler); +} + +function unregisterWrittenHandler(id) { + _writtenHandlers.delete(id); +} + +export { + nativeEventEmitter, + getNextId, + getTcpDataBridge, + registerReadableHandler, + unregisterReadableHandler, + registerWriteDrainHandler, + unregisterWriteDrainHandler, + registerWrittenHandler, + unregisterWrittenHandler, +}; diff --git a/src/NativeTcpDataBridge.ts b/src/NativeTcpDataBridge.ts new file mode 100644 index 0000000..7aad275 --- /dev/null +++ b/src/NativeTcpDataBridge.ts @@ -0,0 +1,37 @@ +/** + * VENHO fork — Phase 1 zero-copy data-plane spec. + * + * This is the codegen contract for the C++ cxxTurboModule. It has ONE + * job: install the `global.__TcpDataBridge` JSI host object into the + * runtime (the quick-base64 install trick). All actual byte movement — + * BOTH directions — happens through that host object's zero-copy + * methods, NEVER through this TurboModule's call path (which would still + * marshal through folly::dynamic, the exact Phase-0/Scenario-C OOM). + * + * The legacy `TcpSockets` NativeModule keeps ONLY the one-shot control + * plane (connect/end/destroy/pause/resume/setNoDelay/setKeepAlive) — + * those are per-socket-lifecycle, not per-byte, so they don't drive the + * dynamicFromValue OOM. The hot `write` data path was MOVED off it onto + * the JSI host object's write() (Phase 1). + */ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + /** + * Installs `global.__TcpDataBridge` (a JSI HostObject) into the JS + * runtime. Idempotent. Returns true on success. Must be called once + * before any socket reads are consumed (Globals.js drives this). + * + * The installed host object exposes (untyped here — JSI host object, + * not codegen, so its bytes never transit folly::dynamic): + * read(id): ArrayBuffer | null // next inbound chunk + * setReadable((id)=>void): void // inbound-ready signal + * write(id, ArrayBuffer, msgId): boolean // zero-copy outbound; + * // false at high watermark + * setWriteDrainable((id)=>void): void // outbound-drained signal + */ + install(): boolean; +} + +export default TurboModuleRegistry.getEnforcing('RNTcpDataBridge'); diff --git a/src/Socket.js b/src/Socket.js index 9913e5a..0531674 100644 --- a/src/Socket.js +++ b/src/Socket.js @@ -3,7 +3,17 @@ import { NativeModules } from 'react-native'; import EventEmitter from 'eventemitter3'; import { Buffer } from 'buffer'; -import { nativeEventEmitter, getNextId } from './Globals'; +import { + nativeEventEmitter, + getNextId, + getTcpDataBridge, + registerReadableHandler, + unregisterReadableHandler, + registerWriteDrainHandler, + unregisterWriteDrainHandler, + registerWrittenHandler, + unregisterWrittenHandler, +} from './Globals'; /** * @typedef {"ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"} BufferEncoding @@ -83,11 +93,16 @@ export default class Socket extends EventEmitter { this._pending = true; /** @private */ this._destroyed = false; + // Set once the local write side has been finished via end()/FIN. + // Drives destroySoon()'s "already flushed?" decision (Node parity). + /** @private */ + this._writableEnded = false; // TODO: Add readOnly and writeOnly states /** @type {'opening' | 'open' | 'readOnly' | 'writeOnly'} @private */ this._readyState = 'open'; // Incorrect, but matches NodeJS behavior - /** @type {{ id: number; data: string; }[]} @private */ - this._pausedDataEvents = []; + // VENHO Phase 1: the old unbounded JS pause buffer is gone. Inbound + // bytes live in the bounded native C++ queue; pausing just stops + // draining it (real TCP backpressure via the native high watermark). this.readableHighWaterMark = 16384; this.writableHighWaterMark = 16384; this.writableNeedDrain = false; @@ -133,6 +148,10 @@ export default class Socket extends EventEmitter { * @param {number} id */ _setId(id) { + // VENHO Phase 1: the readable handler is keyed by socket id. Drop + // the old id's registration BEFORE the id changes, else it leaks in + // the Globals handler map (server-accepted sockets re-id here). + this._unregisterEvents(); this._id = id; this._registerEvents(); } @@ -145,6 +164,7 @@ export default class Socket extends EventEmitter { this._connecting = false; this._readyState = 'open'; this._pending = false; + this._writableEnded = false; this.localAddress = connectionInfo.localAddress; this.localPort = connectionInfo.localPort; this.remoteAddress = connectionInfo.remoteAddress; @@ -157,21 +177,62 @@ export default class Socket extends EventEmitter { * @param {() => void} [callback] */ connect(options, callback) { - const customOptions = { ...options }; - // Normalize args - customOptions.host = customOptions.host || 'localhost'; - customOptions.port = Number(customOptions.port) || 0; + // VENHO Phase 1 — Scenario-C OOM ROOT-CAUSE FIX. + // + // The upstream code did `const customOptions = { ...options }` and + // forwarded that whole object as the 4th arg of the + // `TcpSockets.connect` (codegen) TurboModule. Callers like + // `@libp2p/tcp` spread their ENTIRE dial-options object into the + // `net.connect()` argument — including `signal` (an AbortSignal), + // `upgrader` (which references the entire libp2p components/registry + // graph), `onProgress`, etc. RN's `jsi::dynamicFromValue` then + // recursively walks that deeply-nested, partially CYCLIC object + // graph into nested `folly::dynamic`, exploding into millions of + // unfreed `` map nodes in seconds (the entire + // Scenario-C Scudo OOM / recursive `folly::dynamic::destroy` + // stack-overflow — proven by direct primitive-counter measurement: + // no JS primitive fired, ~4M folly/5s from ONE connect() on a + // blackhole dial). The native side (`TcpSocketClient.connect` / + // `TcpSocketModule`) only ever reads a fixed set of SCALAR option + // keys and a `tls`/`tlsOptions` map — it never touches `signal`, + // `upgrader`, or any caller internals. So we cross ONLY those + // allow-listed primitives; arbitrary caller objects never reach the + // bridge. (Upstream-worthy hardening — part of the #209 fork PR.) + const host = options?.host || 'localhost'; + const port = Number(options?.port) || 0; + /** @type {Record} */ + const customOptions = { host, port }; + // Exactly the keys the native connect path consumes (scalars + + // the tls sub-config map). Copied individually with type coercion; + // never a blanket spread of the caller's object. + if (typeof options?.localAddress === 'string') + customOptions.localAddress = options.localAddress; + if (options?.localPort != null) customOptions.localPort = Number(options.localPort); + if (typeof options?.interface === 'string') customOptions.interface = options.interface; + if (typeof options?.reuseAddress === 'boolean') + customOptions.reuseAddress = options.reuseAddress; + if (options?.connectTimeout != null) + customOptions.connectTimeout = Number(options.connectTimeout); + if (typeof options?.tls === 'boolean') customOptions.tls = options.tls; + if (typeof options?.tlsCheckValidity === 'boolean') + customOptions.tlsCheckValidity = options.tlsCheckValidity; + // tlsCert may be a string (PEM) or a small RN-asset descriptor + // object; pass through only if present (it is bounded, not a + // caller-internals graph). + if (options?.tlsCert != null) customOptions.tlsCert = options.tlsCert; + // `allowHalfOpen` is a connect/constructor option in Node's net API + // and `@libp2p/tcp` passes it straight through `net.connect(cOpts)` + // (it sets `options.allowHalfOpen ?? false` then spreads it). It is + // a JS-side socket-lifecycle flag (governs whether an inbound FIN + // auto-ends our side) — it must NOT cross to native, so it is read + // here, not added to `customOptions`. (#209 / #183 parity.) + if (typeof options?.allowHalfOpen === 'boolean') this.allowHalfOpen = options.allowHalfOpen; this.once('connect', () => { if (callback) callback(); }); this._connecting = true; this._readyState = 'opening'; - NativeModules.TcpSockets.connect( - this._id, - customOptions.host, - customOptions.port, - customOptions - ); + NativeModules.TcpSockets.connect(this._id, host, port, customOptions); return this; } @@ -296,30 +357,77 @@ export default class Socket extends EventEmitter { * @param {BufferEncoding} [encoding] */ end(data, encoding) { + if (this._pending || this._destroyed) return this; if (data) { + this._writableEnded = true; this.write(data, encoding, () => { NativeModules.TcpSockets.end(this._id); }); return this; } - if (this._pending || this._destroyed) return this; this._clearTimeout(); + this._writableEnded = true; NativeModules.TcpSockets.end(this._id); return this; } /** * Ensures that no more I/O activity happens on this socket. Destroys the stream and closes the connection. + * + * @param {Error} [error] Optional error; if given, emitted as an `'error'` event before `'close'`. */ - destroy() { + destroy(error) { if (this._destroyed) return this; this._destroyed = true; this._clearTimeout(); NativeModules.TcpSockets.destroy(this._id); + if (error) this.emit('error', error); return this; } + /** + * Half-closes the socket (sends FIN) and destroys it once the write + * side has finished flushing. Mirrors Node's `net.Socket.destroySoon`: + * if the socket is already finished writing it is destroyed at once, + * otherwise it is `end()`-ed and torn down on `'finish'`/`'close'`. + * + * `@libp2p/tcp` calls this on every graceful connection close + * (`sendClose` → `socket.destroySoon()` then awaits `'close'`); the + * method MUST exist or every libp2p connection teardown throws + * `TypeError: socket.destroySoon is not a function`. (#209) + * + * @returns {this} + */ + destroySoon() { + if (this._destroyed) return this; + // Already flushed (writable finished) → tear down immediately. + if (this._writableEnded && this._writeBufferSize === 0) { + this.destroy(); + return this; + } + // Otherwise FIN now, then destroy when the OS reports the socket + // closed (native emits 'close' after the FIN/peer-close completes). + this.once('close', () => this.destroy()); + this.end(); + return this; + } + + /** + * Closes the TCP connection by sending an RST packet and destroying + * the stream. The underlying native module exposes only a graceful + * close, so this maps to `destroy()` — the closest available teardown + * (an abortive RST is not separately expressible on the native side). + * Present for Node `net.Socket` API parity: `@libp2p/tcp`'s + * `sendReset` calls `socket.resetAndDestroy()`. (#209) + * + * @returns {this} + */ + resetAndDestroy() { + if (this._destroyed) return this; + return this.destroy(); + } + /** * Sends data on the socket. The second parameter specifies the encoding in the case of a string — it defaults to UTF8 encoding. * @@ -360,11 +468,41 @@ export default class Socket extends EventEmitter { }; // Callback equivalent with better performance this._msgEvtEmitter.on('written', msgEvtHandler, this); - const ok = this._writeBufferSize < this.writableHighWaterMark; - if (!ok) this.writableNeedDrain = true; + let ok = this._writeBufferSize < this.writableHighWaterMark; this._lastSentMsgId = currentMsgId; this._bytesWritten += generatedBuffer.byteLength; - NativeModules.TcpSockets.write(this._id, generatedBuffer.toString('base64'), currentMsgId); + // VENHO Phase 1: zero-(legacy-)copy outbound. Push the bytes through + // the JSI data bridge instead of NativeModules.TcpSockets.write(id, + // base64, msgId) — that base64 String arg crossed the Java + // TurboModule's jsi::dynamicFromValue per write and was the residual + // Scenario-C Scudo OOM. The host fn copies the bytes ONCE off this + // ArrayBuffer into the bounded C++ outbound queue (no base64, no + // folly::dynamic); it returns false at the queue's high watermark, + // which (in addition to the JS-side _writeBufferSize check) latches + // backpressure until the native write loop drains and the + // write-drain handler clears it. + const bridge = typeof getTcpDataBridge === 'function' ? getTcpDataBridge() : null; + if (bridge && typeof bridge.write === 'function') { + // Hermes Buffer is a Uint8Array view; hand the exact byte range as + // its own ArrayBuffer (sliced — the host copies synchronously, so + // a tight buffer is correct and avoids leaking the pool). + const ab = generatedBuffer.buffer.slice( + generatedBuffer.byteOffset, + generatedBuffer.byteOffset + generatedBuffer.byteLength + ); + const nativeOk = bridge.write(this._id, ab, currentMsgId); + if (nativeOk === false) ok = false; + } else { + // No JSI data bridge means the native install() did not run — a + // hard wiring error, not a recoverable state. Fail loud rather + // than silently dropping outbound bytes (which previously + // masqueraded as a mystery leak). + throw new Error( + '[react-native-tcp-socket] JSI TcpDataBridge unavailable — ' + + 'native install() did not run (clean rebuild needed?)' + ); + } + if (!ok) this.writableNeedDrain = true; return ok; } @@ -386,7 +524,10 @@ export default class Socket extends EventEmitter { if (!this._paused) return; this._paused = false; this.emit('resume'); - this._recoverDataEventsAfterPause(); + // VENHO Phase 1: nothing buffered in JS anymore. Tell native to + // resume its read loop, then drain whatever the C++ queue holds. + NativeModules.TcpSockets.resume(this._id); + this._drainInbound(); } ref() { @@ -398,68 +539,73 @@ export default class Socket extends EventEmitter { } /** + * VENHO Phase 1 — zero-copy inbound drain. + * + * Invoked (by socket id) from the single JSI readable callback that + * the C++ TcpDataBridge hops onto the JS thread via the CallInvoker — + * NOT a device event. Native pushed the bytes into the bounded C++ + * TcpInboundRegistry; here we pull them zero-copy: each `read(id)` + * returns an ArrayBuffer whose storage IS the received bytes (no + * base64, no folly::dynamic, no per-chunk legacy-bridge crossing — + * milestone 1d proved any such crossing OOMs). Buffer.from(ab) wraps + * without copying (@craftzdog/react-native-buffer). + * + * Backpressure: while `_paused` we STOP draining; bytes stay in the + * bounded native queue, which pauses the socket read at its high + * watermark (real TCP backpressure). The old unbounded + * `_pausedDataEvents` array is gone. + * * @private */ - async _recoverDataEventsAfterPause() { - if (this._resuming) return; - this._resuming = true; - while (this._pausedDataEvents.length > 0) { - // Concat all buffered events for better performance - const buffArray = []; - let readBytes = 0; - let i = 0; - for (; i < this._pausedDataEvents.length; i++) { - const evtData = Buffer.from(this._pausedDataEvents[i].data, 'base64'); - readBytes += evtData.byteLength; - if (readBytes <= this.readableHighWaterMark) { - buffArray.push(evtData); - } else { - const buffOffset = this.readableHighWaterMark - readBytes; - buffArray.push(evtData.slice(0, buffOffset)); - this._pausedDataEvents[i].data = evtData.slice(buffOffset).toString('base64'); - break; - } - } - // Generate new event with the concatenated events - const evt = { - id: this._pausedDataEvents[0].id, - data: Buffer.concat(buffArray).toString('base64'), - }; - // Clean the old events - this._pausedDataEvents = this._pausedDataEvents.slice(i); - this._onDeviceDataEvt(evt); - if (this._paused) { - this._resuming = false; - return; - } - } - this._resuming = false; - NativeModules.TcpSockets.resume(this._id); - } - - /** - * @private - */ - _onDeviceDataEvt = (/** @type {{ id: number; data: string; }} */ evt) => { - if (evt.id !== this._id) return; + _drainInbound() { + if (this._paused || this._destroyed) return; + const bridge = getTcpDataBridge(); + if (!bridge) return; this._resetTimeout(); - if (!this._paused) { - const bufferData = Buffer.from(evt.data, 'base64'); + // Drain everything currently queued for this socket id. + for (;;) { + if (this._paused || this._destroyed) return; + const ab = bridge.read(this._id); + if (ab == null) return; + const bufferData = Buffer.from(ab); this._bytesRead += bufferData.byteLength; const finalData = this._encoding ? bufferData.toString(this._encoding) : bufferData; this.emit('data', finalData); - } else { - // If the socket is paused, save the data events for later - this._pausedDataEvents.push(evt); } - }; + } /** * @private */ _registerEvents() { this._unregisterEvents(); - this._dataListener = this._eventEmitter.addListener('data', this._onDeviceDataEvt); + // VENHO Phase 1: the readable signal arrives via the JSI bridge (a + // single C++→JS CallInvoker callback dispatched by socket id), NOT a + // device event — milestone 1d proved any per-chunk legacy-bridge + // crossing OOMs. Register this socket's drain handler; bytes are + // then pulled zero-copy via the JSI read(id) in _drainInbound. + // Ensures the bridge (and its single setReadable cb) is installed. + // Guarded: the Jest env mocks ./Globals with only a subset of + // exports — degrade to a no-op there (the suite asserts control + // plane, not the JSI data path). + if (typeof getTcpDataBridge === 'function') getTcpDataBridge(); + if (typeof registerReadableHandler === 'function') { + registerReadableHandler(this._id, () => this._drainInbound()); + } + // VENHO Phase 1: the native write loop fires this (via the JSI + // CallInvoker, NOT a device event) when the C++ outbound queue + // drained below its low watermark. Release backpressure: clear the + // need-drain latch and emit `drain` so libp2p/Node streams resume + // writing. (The per-write `written` ack still flows through the + // event path below — that's a tiny {id,msgId} map, not the leak.) + if (typeof registerWriteDrainHandler === 'function') { + registerWriteDrainHandler(this._id, () => { + if (this.writableNeedDrain) { + this.writableNeedDrain = false; + this.emit('drain'); + } + }); + } this._errorListener = this._eventEmitter.addListener('error', (evt) => { if (evt.id !== this._id) return; this.destroy(); @@ -468,7 +614,13 @@ export default class Socket extends EventEmitter { this._closeListener = this._eventEmitter.addListener('close', (evt) => { if (evt.id !== this._id) return; this._setDisconnected(); - this.emit('close', evt.error); + // Node's net.Socket 'close' passes a BOOLEAN `hadError`, not the + // error object (the error itself is delivered via 'error', emitted + // first). `@libp2p/tcp` relies on this exact shape: + // `socket.once('close', hadError => { if (hadError) abort(...) })`. + // Previously this emitted the raw error and only worked by + // truthiness — now spec-correct. (#209 Node-parity) + this.emit('close', Boolean(evt.error)); }); this._endListener = this._eventEmitter.addListener('end', (evt) => { if (evt.id !== this._id) return; @@ -482,22 +634,48 @@ export default class Socket extends EventEmitter { this._setConnected(evt.connection); this.emit('connect'); }); - this._writtenListener = this._eventEmitter.addListener('written', (evt) => { - if (evt.id !== this._id) return; - this._msgEvtEmitter.emit('written', evt); - }); + // VENHO Phase 1: the per-write `written` ACK now arrives via the JSI + // CallInvoker (single C++→JS callback keyed by socket id), NOT the + // legacy RCTDeviceEventEmitter `written` device event — that + // per-write emit accumulated an unbounded folly::dynamic in the + // bridgeless event-emitter queue (Scenario-C OOM #2). The handler + // feeds the SAME in-JS `_msgEvtEmitter` the per-write `msgEvtHandler` + // already listens on, so all the existing ack/drain/callback + // accounting is unchanged. Guarded for the Jest mock env. + if (typeof registerWrittenHandler === 'function') { + registerWrittenHandler(this._id, (msgId, err) => { + this._msgEvtEmitter.emit('written', { + id: this._id, + msgId, + // C++ passes '' for success; normalise to undefined so the + // existing `if (err)` checks behave exactly as before. + err: err ? err : undefined, + }); + }); + } } /** * @package */ _unregisterEvents() { - this._dataListener?.remove(); + // VENHO Phase 1: readable is a JSI handler keyed by socket id, not a + // device-event subscription. Guarded for the Jest mock env. + if (typeof unregisterReadableHandler === 'function') { + unregisterReadableHandler(this._id); + } + if (typeof unregisterWriteDrainHandler === 'function') { + unregisterWriteDrainHandler(this._id); + } + // VENHO Phase 1: `written` is a JSI handler keyed by socket id, not + // a device-event subscription. Guarded for the Jest mock env. + if (typeof unregisterWrittenHandler === 'function') { + unregisterWrittenHandler(this._id); + } this._errorListener?.remove(); this._closeListener?.remove(); this._endListener?.remove(); this._connectListener?.remove(); - this._writtenListener?.remove(); } /**