Skip to content

devioarts/capacitor-tcpclient

Repository files navigation

@devioarts/capacitor-tcpclient

TCP Client for Capacitor with iOS/Android/Electron support - Example App

Install

npm install @devioarts/capacitor-tcpclient
npx cap sync

Android

/android/app/src/main/AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

iOS

/ios/App/App/Info.plist

<key>NSLocalNetworkUsageDescription</key>
<string>It is needed for the correct functioning of the application</string>

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsLocalNetworking</key>
  <true/>
</dict>

ElectronJS

Implementation example was developed on capacitor-electron base, if you run electron differently, you may need to adjust the code.

/electron/main.ts

// ...
// THIS LINE IS IMPORTANT FOR PLUGIN!
import {TCPClient} from "@devioarts/capacitor-tcpclient/electron/tcpclient";
// ...
// THIS LINE IS IMPORTANT FOR PLUGIN!
let tcpClient: TCPClient | null = null;
// ...
function createWindow() {
  const win = new BrowserWindow(
    // ...
  );
  // ...
  // THIS LINE IS IMPORTANT FOR PLUGIN!
  tcpClient = new TCPClient(win);
  // ...
}
// ...

electron/preload.cjs

const { contextBridge, ipcRenderer } = require("electron");

// THIS LINE IS IMPORTANT FOR PLUGIN!
const {createTCPClientAPI} = require("@devioarts/capacitor-tcpclient/electron/tcpclient-bridge.cjs");
// ...
// THIS LINE IS IMPORTANT FOR PLUGIN!
contextBridge.exposeInMainWorld('TCPClient', createTCPClientAPI({ ipcRenderer }));
// ...

Technical behavior & guarantees

  • Platforms: iOS / Android / Electron — same API and return shapes.
  • Request/Response (writeAndRead)
    • Without expect: returns after until-idle (adaptive ~50–200 ms) to capture the full reply.
    • With expect: returns on first match. If timeout expires and some data arrived, returns success with matched:false; if no data arrived, returns a timeout error.
  • Timeouts: timeout is the total RR budget. readTimeout exists only on Android (continuous reader); on iOS/Electron it’s a no-op for API parity.
  • Streaming (tcpData events): micro-batched every 10 ms or 16 KB; on Electron the batch is split by your chunkSize before it’s sent to the web layer.
  • Bytes & flags: bytesSent = actually written; on RR timeout it remains the request length, on other errors it’s 0. bytesReceived = length of returned data. matched = whether expect was found.
  • Connectivity (tcpIsConnected): fast socket check. If RR/stream is running it returns true; otherwise it performs an active peek/EOF check and emits tcpDisconnect on remote close.
  • Stream suspension: suspendStreamDuringRR (default true) temporarily detaches streaming so the RR read can’t be “stolen” by the stream consumer.
  • Security: plain TCP only (no TLS). Use an external TLS terminator (e.g., stunnel) if you need TLS.

FAQ

  • Why “until-idle” without expect? Many devices reply in fragments; a short adaptive idle window (~50–200 ms) avoids cutting responses.
  • Why success on expect + timeout (with data)? To avoid dropping partial replies; matched:false tells you the pattern didn’t occur.
  • Why readTimeout only on Android? Android Socket uses blocking I/O where SO_TIMEOUT matters; iOS/Electron use evented reads.

Minimal usage (recap)

import { TCPClient } from '@devioarts/capacitor-tcpclient';

await TCPClient.tcpConnect({ host: '192.168.1.100', port: 9100, timeout: 3000 });

// stream (micro-batch 10 ms / 16 KB; split by chunkSize on Electron)
await TCPClient.tcpStartRead({ chunkSize: 4096 });
TCPClient.addListener('tcpData', ({ data }) => {
  console.log('RX:', data.length);
});

// RR
const rr = await TCPClient.tcpWriteAndRead({
  data: [0x1b, 0x40],
  timeout: 1000,
  maxBytes: 4096,
  // expect: '1b40' | [0x1b, 0x40]
  suspendStreamDuringRR: true,
});
console.log(rr.error ? rr.errorMessage : { matched: rr.matched, bytes: rr.bytesReceived });

await TCPClient.tcpDisconnect();

API

connect(...)

connect(options: TcpConnectOptions) => Promise<TcpConnectResult>

Open a TCP connection.

Param Type
options TcpConnectOptions

Returns: Promise<TcpConnectResult>


disconnect()

disconnect() => Promise<TcpDisconnectResult>

Close the TCP connection. Idempotent. Triggers tcpDisconnect(manual).

Returns: Promise<TcpDisconnectResult>


isConnected()

isConnected() => Promise<TcpIsConnectedResult>

Check whether the socket is connected.

Returns: Promise<TcpIsConnectedResult>


isReading()

isReading() => Promise<TcpIsReadingResult>

Check whether the stream reader is active.

Returns: Promise<TcpIsReadingResult>


write(...)

write(options: TcpWriteOptions) => Promise<TcpWriteResult>

Write raw bytes.

Param Type
options TcpWriteOptions

Returns: Promise<TcpWriteResult>


writeAndRead(...)

writeAndRead(options: TcpWriteAndReadOptions) => Promise<TcpWriteAndReadResult>

Write request, then read reply under the given constraints.

Param Type
options TcpWriteAndReadOptions

Returns: Promise<TcpWriteAndReadResult>


startRead(...)

startRead(options?: TcpStartReadOptions | undefined) => Promise<TcpStartStopResult>

Start emitting tcpData events. Safe to call multiple times.

Param Type
options TcpStartReadOptions

Returns: Promise<TcpStartStopResult>


stopRead()

stopRead() => Promise<TcpStartStopResult>

Stop emitting tcpData events. Safe to call multiple times.

Returns: Promise<TcpStartStopResult>


setReadTimeout(...)

setReadTimeout(options: { readTimeout: number; }) => Promise<{ error: boolean; errorMessage?: string | null; }>

Configure stream read timeout (Android only). iOS: no-op; Electron: stored for RR defaults. Provided for API parity across platforms.

Param Type
options { readTimeout: number; }

Returns: Promise<{ error: boolean; errorMessage?: string | null; }>


addListener('tcpData', ...)

addListener(eventName: 'tcpData', listenerFunc: (event: TcpDataEvent) => void) => Promise<PluginListenerHandle>

Subscribe to micro-batched stream data events.

Param Type
eventName 'tcpData'
listenerFunc (event: TcpDataEvent) => void

Returns: Promise<PluginListenerHandle>


addListener('tcpDisconnect', ...)

addListener(eventName: 'tcpDisconnect', listenerFunc: (event: TcpDisconnectEvent) => void) => Promise<PluginListenerHandle>

Subscribe to disconnect notifications.

Param Type
eventName 'tcpDisconnect'
listenerFunc (event: TcpDisconnectEvent) => void

Returns: Promise<PluginListenerHandle>


removeAllListeners()

removeAllListeners() => Promise<void>

Remove all tcpData/tcpDisconnect listeners.


Interfaces

TcpConnectResult

Result of connect().

  • connected=true on success; false on failure.
  • error=true with errorMessage on failure (e.g., "connect timeout", "connect failed: ...").
Prop Type
error boolean
errorMessage string | null
connected boolean

TcpConnectOptions

Connection parameters for opening a TCP socket.

Notes by platform:

  • Android: validates port range (1..65535); applies TCP_NODELAY and SO_KEEPALIVE according to the flags. Connect timeout is enforced by Socket#connect.
  • iOS: sets TCP_NODELAY, SO_KEEPALIVE and SO_NOSIGPIPE. Connect timeout is enforced using non-blocking connect + polling.
  • Electron: sets noDelay and keepAlive (with 60s initial delay). Connect timeout is emulated via a JS timer that destroys the socket if elapsed.
Prop Type Description
host string Hostname or IP address to connect to. Required.
port number TCP port, defaults to 9100. Valid range 1..65535 (validated on Android).
timeout number Connect timeout in milliseconds, defaults to 3000.
noDelay boolean Enable TCP_NODELAY (Nagle off). Defaults to true.
keepAlive boolean Enable SO_KEEPALIVE. Defaults to true.

TcpDisconnectResult

Result of disconnect(). Always resolves. After disconnect, reading is false. A tcpDisconnect event with reason 'manual' is also emitted by platforms.

Prop Type Description
error boolean
errorMessage string | null
disconnected boolean True if the instance transitioned to disconnected state.
reading boolean Whether the stream reader is active (always false after disconnect).

TcpIsConnectedResult

Result of isConnected().

  • Android performs a safe 1-byte peek unless streaming/RR is active, in which case it returns true if those are active to avoid consuming input.
  • iOS/Electron return based on current socket open/close state.
Prop Type
error boolean
errorMessage string | null
connected boolean

TcpIsReadingResult

Result of isReading(). True if stream reader is active.

Prop Type
error boolean
errorMessage string | null
reading boolean

TcpWriteResult

Result of write().

  • bytesSent equals the request length on success; 0 on failure.
  • Fails with error=true if not connected or busy (RR in progress on some platforms).
Prop Type
error boolean
errorMessage string | null
bytesSent number

TcpWriteOptions

Bytes to write to the socket verbatim. Accepts number[] or Uint8Array.

Prop Type
data number[] | Uint8Array

Uint8Array

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Prop Type Description
BYTES_PER_ELEMENT number The size in bytes of each element in the array.
buffer ArrayBufferLike The ArrayBuffer instance referenced by the array.
byteLength number The length in bytes of the array.
byteOffset number The offset in bytes of the array.
length number The length of the array.
Method Signature Description
copyWithin (target: number, start: number, end?: number | undefined) => this Returns the this object after copying a section of the array identified by start and end to the same array starting at position target
every (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any) => boolean Determines whether all the members of an array satisfy the specified test.
fill (value: number, start?: number | undefined, end?: number | undefined) => this Returns the this object after filling the section identified by start and end with value
filter (predicate: (value: number, index: number, array: Uint8Array) => any, thisArg?: any) => Uint8Array Returns the elements of an array that meet the condition specified in a callback function.
find (predicate: (value: number, index: number, obj: Uint8Array) => boolean, thisArg?: any) => number | undefined Returns the value of the first element in the array where predicate is true, and undefined otherwise.
findIndex (predicate: (value: number, index: number, obj: Uint8Array) => boolean, thisArg?: any) => number Returns the index of the first element in the array where predicate is true, and -1 otherwise.
forEach (callbackfn: (value: number, index: number, array: Uint8Array) => void, thisArg?: any) => void Performs the specified action for each element in an array.
indexOf (searchElement: number, fromIndex?: number | undefined) => number Returns the index of the first occurrence of a value in an array.
join (separator?: string | undefined) => string Adds all the elements of an array separated by the specified separator string.
lastIndexOf (searchElement: number, fromIndex?: number | undefined) => number Returns the index of the last occurrence of a value in an array.
map (callbackfn: (value: number, index: number, array: Uint8Array) => number, thisArg?: any) => Uint8Array Calls a defined callback function on each element of an array, and returns an array that contains the results.
reduce (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number) => number Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
reduce (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number, initialValue: number) => number
reduce <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8Array) => U, initialValue: U) => U Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
reduceRight (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number) => number Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
reduceRight (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number, initialValue: number) => number
reduceRight <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8Array) => U, initialValue: U) => U Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
reverse () => Uint8Array Reverses the elements in an Array.
set (array: ArrayLike<number>, offset?: number | undefined) => void Sets a value or an array of values.
slice (start?: number | undefined, end?: number | undefined) => Uint8Array Returns a section of an array.
some (predicate: (value: number, index: number, array: Uint8Array) => unknown, thisArg?: any) => boolean Determines whether the specified callback function returns true for any element of an array.
sort (compareFn?: ((a: number, b: number) => number) | undefined) => this Sorts an array.
subarray (begin?: number | undefined, end?: number | undefined) => Uint8Array Gets a new Uint8Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.
toLocaleString () => string Converts a number to a string by using the current locale.
toString () => string Returns a string representation of an array.
valueOf () => Uint8Array Returns the primitive value of the specified object.

ArrayLike

Prop Type
length number

ArrayBufferTypes

Allowed ArrayBuffer types for the buffer of an ArrayBufferView and related Typed Arrays.

Prop Type
ArrayBuffer ArrayBuffer

ArrayBuffer

Represents a raw buffer of binary data, which is used to store data for the different typed arrays. ArrayBuffers cannot be read from or written to directly, but can be passed to a typed array or DataView Object to interpret the raw buffer as needed.

Prop Type Description
byteLength number Read-only. The length of the ArrayBuffer (in bytes).
Method Signature Description
slice (begin: number, end?: number | undefined) => ArrayBuffer Returns a section of an ArrayBuffer.

TcpWriteAndReadResult

Result of writeAndRead().

  • bytesSent is the number of request bytes written. If the operation fails due to a pure timeout (no bytes received), bytesSent can still equal the request length; for other errors it is 0.
  • bytesReceived is the length of returned data (<= maxBytes).
  • matched indicates whether the expect pattern (if any) was found.
Prop Type Description
error boolean
errorMessage string | null
bytesSent number
bytesReceived number
data number[] Received bytes (may be partial if timeout after some data).
matched boolean True if the expect pattern was matched; false otherwise.

TcpWriteAndReadOptions

Options for writeAndRead() request/response operation.

Behavior summary (parity across Android/iOS/Electron):

  • The request is written atomically with internal serialization (no interleaved writes across concurrent calls).
  • Response collection ends when ANY of these happens: • expect pattern is found (matched=true), or • maxBytes cap is reached, or • without expect: adaptive "until-idle" period elapses after last data, or • absolute timeout elapses (see errors below).
  • On timeout: • If no data arrived at all, the call fails with error=true and errorMessage resembling "connect timeout" and bytesSent equals the request length on Android/iOS/Electron; bytesReceived=0, matched=false. • If some data arrived before the deadline, the call resolves successfully with matched=false and returns the partial data.
  • suspendStreamDuringRR: when true, the active stream reader is temporarily stopped for the RR window to avoid racing over the same bytes; after RR it is resumed with the previous chunk size. Default is true on Android & iOS; Electron treats it as true by default as well.
  • expect: hex string like "0A0B0C" (case/spacing ignored) or a byte array.
Prop Type Description
data number[] | Uint8Array Request payload to send.
timeout number Absolute RR timeout in ms. Defaults to 1000.
maxBytes number Maximum number of bytes to accumulate and return. Defaults to 4096.
expect string | number[] | Uint8Array Optional expected pattern. When provided, reading stops as soon as the accumulated buffer contains this pattern. Accepts: - number[] / Uint8Array: raw byte sequence - string: hex bytes (e.g., "0x1b40", "1B 40"), spacing and case ignored
suspendStreamDuringRR boolean Temporarily suspend the stream reader during RR to avoid consuming reply in the stream. Defaults to true (Android default true; iOS behaves as if true; Electron defaults to true as well).

TcpStartStopResult

Result of startRead()/stopRead().

Prop Type Description
error boolean
errorMessage string | null
reading boolean Whether the stream reader is currently active.

TcpStartReadOptions

Options for startRead().

  • chunkSize controls maximum size of a single tcpData event slice. Native implementations may micro-batch multiple small reads; Electron additionally splits a flushed batch into slices up to chunkSize to preserve consumer expectations.
  • readTimeout applies only on Android (socket SO_TIMEOUT while streaming). It is a no-op on iOS. Electron stores it for RR but does not apply to stream.
Prop Type Description
chunkSize number Maximum bytes per emitted tcpData event. Default 4096.
readTimeout number Stream read timeout (ms). Android: applies SO_TIMEOUT; iOS: no-op.

PluginListenerHandle

Prop Type
remove () => Promise<void>

TcpDataEvent

Emitted by the stream reader with micro-batched data chunks.

  • Data values are 0..255. The plugin may coalesce multiple small reads and then emit one or more events capped by chunkSize.
Prop Type
data number[]

TcpDisconnectEvent

Emitted when the socket is closed or the plugin disconnects it.

  • reason: • 'manual' — disconnect() called or instance disposed. • 'remote' — the peer closed the connection (EOF). • 'error' — an I/O error occurred; error contains a message.
  • reading is false when this event fires.
Prop Type
disconnected true
reading boolean
reason 'error' | 'manual' | 'remote'
error string

Type Aliases

ArrayBufferLike

ArrayBufferTypes[keyof ArrayBufferTypes]

About

TCP Client for Capacitor working on Android, iOS and Electron

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors