TCP Client for Capacitor with iOS/Android/Electron support - Example App
npm install @devioarts/capacitor-tcpclient
npx cap sync<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><key>NSLocalNetworkUsageDescription</key>
<string>It is needed for the correct functioning of the application</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>Implementation example was developed on capacitor-electron base, if you run electron differently, you may need to adjust the code.
// ...
// 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);
// ...
}
// ...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 }));
// ...- 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. Iftimeoutexpires and some data arrived, returns success withmatched:false; if no data arrived, returns a timeout error.
- Without
- Timeouts:
timeoutis the total RR budget.readTimeoutexists only on Android (continuous reader); on iOS/Electron it’s a no-op for API parity. - Streaming (
tcpDataevents): micro-batched every 10 ms or 16 KB; on Electron the batch is split by yourchunkSizebefore it’s sent to the web layer. - Bytes & flags:
bytesSent= actually written; on RR timeout it remains the request length, on other errors it’s0.bytesReceived= length of returneddata.matched= whetherexpectwas found. - Connectivity (
tcpIsConnected): fast socket check. If RR/stream is running it returnstrue; otherwise it performs an active peek/EOF check and emitstcpDisconnecton 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.
- 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:falsetells you the pattern didn’t occur. - Why
readTimeoutonly on Android? AndroidSocketuses blocking I/O whereSO_TIMEOUTmatters; iOS/Electron use evented reads.
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();connect(...)disconnect()isConnected()isReading()write(...)writeAndRead(...)startRead(...)stopRead()setReadTimeout(...)addListener('tcpData', ...)addListener('tcpDisconnect', ...)removeAllListeners()- Interfaces
- Type Aliases
connect(options: TcpConnectOptions) => Promise<TcpConnectResult>Open a TCP connection.
| Param | Type |
|---|---|
options |
TcpConnectOptions |
Returns: Promise<TcpConnectResult>
disconnect() => Promise<TcpDisconnectResult>Close the TCP connection. Idempotent. Triggers tcpDisconnect(manual).
Returns: Promise<TcpDisconnectResult>
isConnected() => Promise<TcpIsConnectedResult>Check whether the socket is connected.
Returns: Promise<TcpIsConnectedResult>
isReading() => Promise<TcpIsReadingResult>Check whether the stream reader is active.
Returns: Promise<TcpIsReadingResult>
write(options: TcpWriteOptions) => Promise<TcpWriteResult>Write raw bytes.
| Param | Type |
|---|---|
options |
TcpWriteOptions |
Returns: Promise<TcpWriteResult>
writeAndRead(options: TcpWriteAndReadOptions) => Promise<TcpWriteAndReadResult>Write request, then read reply under the given constraints.
| Param | Type |
|---|---|
options |
TcpWriteAndReadOptions |
Returns: Promise<TcpWriteAndReadResult>
startRead(options?: TcpStartReadOptions | undefined) => Promise<TcpStartStopResult>Start emitting tcpData events. Safe to call multiple times.
| Param | Type |
|---|---|
options |
TcpStartReadOptions |
Returns: Promise<TcpStartStopResult>
stopRead() => Promise<TcpStartStopResult>Stop emitting tcpData events. Safe to call multiple times.
Returns: Promise<TcpStartStopResult>
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(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(eventName: 'tcpDisconnect', listenerFunc: (event: TcpDisconnectEvent) => void) => Promise<PluginListenerHandle>Subscribe to disconnect notifications.
| Param | Type |
|---|---|
eventName |
'tcpDisconnect' |
listenerFunc |
(event: TcpDisconnectEvent) => void |
Returns: Promise<PluginListenerHandle>
removeAllListeners() => Promise<void>Remove all tcpData/tcpDisconnect listeners.
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 |
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. |
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). |
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 |
Result of isReading(). True if stream reader is active.
| Prop | Type |
|---|---|
error |
boolean |
errorMessage |
string | null |
reading |
boolean |
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 |
Bytes to write to the socket verbatim. Accepts number[] or Uint8Array.
| Prop | Type |
|---|---|
data |
number[] | 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. |
| Prop | Type |
|---|---|
length |
number |
Allowed ArrayBuffer types for the buffer of an ArrayBufferView and related Typed Arrays.
| Prop | Type |
|---|---|
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. |
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. |
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). |
Result of startRead()/stopRead().
| Prop | Type | Description |
|---|---|---|
error |
boolean |
|
errorMessage |
string | null |
|
reading |
boolean |
Whether the stream reader is currently active. |
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. |
| Prop | Type |
|---|---|
remove |
() => Promise<void> |
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[] |
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 |
ArrayBufferTypes[keyof ArrayBufferTypes]