Introduction
The tcpserver plugin is a complete callback-based TCP package: an `Open` call opens a loopback TCP server (incoming connections are dispatched to a callback), and the SAME plugin can also act as a client — it can send a request to its own (or another) listener via `RequestText`/`RequestBlob` (synchronous) or `BeginTextRequest`/`BeginBlobRequest` (asynchronous) calls. The plugin supports the full asyncmeta vocabulary (Capabilities = 4095 = ProfileRouteService), and every job-level query has two names: the native `Request*` and the asyncmeta-compatible `Job*` synonym.
Server + client in one
This plugin is both a TCP server AND a TCP client. `Open(callback, timeoutMs)` opens a loopback TCP port and dispatches the RAW bytes of every incoming connection to the callback on a background thread. The callback signature is `callback i32 NAME(ref blob $Request)` (see the extended forms below for the peer's address and certificate); the callback may modify the request blob in place, and the modified bytes become the TCP response. Meanwhile, `RequestText(listenerId, text, timeoutMs)` synchronously and `BeginTextRequest(listenerId, text, timeoutMs)` asynchronously open a TCP connection to the listener's port, send the text/blob, and return the response (or an asynchronous requestId).
Peer metadata: the extended callback forms
The handler may declare extra parameters, and the host fills them in with the peer's identity. Three forms are accepted; pick the shortest one that covers what you need:
callback i32 NAME(ref blob $Request)— payload only.callback i32 NAME(ref blob $Request, string $PeerHost, i32 $PeerPort)— plus the client's address and port.callback i32 NAME(ref blob $Request, string $PeerHost, i32 $PeerPort, string $PeerCertSubject)— plus the client certificate's subject when the connection is mTLS-verified. It is an empty string when there is no TLS, or when an optional-mode client presented no certificate.
The same forms work on the TCP, HTTP and UDP callback families, so a handler moves between transports unchanged.
What is the client side for?
The `RequestText` / `Begin*Request` families exist primarily for TESTING: you call your own server with them to confirm the callback handles requests correctly. The loopback TCP channel has minimal overhead and works like a locally simulated incoming connection. They can also reach other loopback servers (e.g. another tcp_server listener, or another local TCP service).
Relationship to asyncmeta
The plugin is fully built on the asyncmeta constants. `Capabilities()` returns exactly the `asyncmeta.ProfileRouteService()` bitmask (=4095) — the entire vocabulary supported. `RouteState` returns `asyncmeta.RouteStateOpen` (=1) / `RouteStateClosed` (=0); `RouteMeta(listenerId, key)` knows the `asyncmeta.RouteMeta*` keys; `JobMeta(requestId, key)` knows the `asyncmeta.JobMeta*` keys.
Request* / Job* synonym pairs
Job-level (i.e., asynchronous request) query functions all exist under two names. `Request*` is the native, descriptive name (paints a “request”, HTTP-like image); `Job*` is the asyncmeta-vocabulary name. The two names map to the EXACT same handler — pick stylistically. Examples: `RequestPhase` ↔︎ `Phase`, `RequestStatus` ↔︎ `Status`, `RequestError` ↔︎ `ErrorText`, `RequestStatusText` ↔︎ `StatusText`, `RequestInfo` ↔︎ `JobInfo`, `RequestRoute` ↔︎ `JobRoute`, `RequestTimeout` ↔︎ `JobTimeout`, `RequestKnown` ↔︎ `JobKnown`, `RequestMeta` ↔︎ `JobMeta`, `RequestHasBlobResult` ↔︎ `JobHasBlobResult`, `RequestBlobResultSize` ↔︎ `JobBlobResultSize`.
Optional TLS layer
`EnableTls(listenerId, certFile, keyFile)` activates TLS on the listener: every subsequent incoming connection is wrapped in TLS by the tls plugin's provider. The listener itself does NOT link OpenSSL — that is resolved at runtime via the tls plugin. On a platform that doesn't support TLS (e.g. Windows pre-bundle), EnableTls returns a clean error and the listener keeps serving plaintext. TLS activation is IDEMPOTENT: calling it again with the same files just returns 0.
Sync vs async request
`RequestText(listenerId, text, timeoutMs)` and `RequestBlob(listenerId, blob, timeoutMs)` are SYNCHRONOUS: the call BLOCKS until the TCP connection is established, the request is sent, and the response arrives. `BeginTextRequest`/`BeginBlobRequest` are ASYNCHRONOUS: they return immediately with a requestId, and a background thread handles the connect/send/wait. Async jobs are managed with `WaitComplete(requestId, timeoutMs)`, `TakeTextResponse(requestId)`, and `TakeBlob(requestId)`.
Loading the plugin
plugin "builtin:print"; plugin "../plugins/async_meta/AsyncMetaPlugin"; plugin "../plugins/tcp_server/TcpServerPlugin";
A typical flow — synchronous echo on loopback
callback i32 Echo(ref blob $Request) { // the callback may modify the request bytes in place; // these become the TCP response bytes return(1); } i32 main() { i64 $lid; string $resp[64]; $lid = tcpserver.Open("Echo", 5000); $resp = tcpserver.RequestText($lid, "PING", 5000); // ... process the response ... tcpserver.Close($lid); return(0); }
A typical flow — asynchronous request with WaitComplete + TakeTextResponse
i64 $lid; i64 $jobId; string $resp[64]; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); // ... the script does other things ... tcpserver.WaitComplete($jobId, 5000); if (tcpserver.Status($jobId) == 0) { $resp = tcpserver.TakeTextResponse($jobId); // ... processing ... } tcpserver.Close($lid);
What to know about every function (the basics)
Capabilities is always ProfileRouteService = 4095 (every asyncmeta bit).
Open binds to loopback (127.0.0.1) on an OS-picked port (query with Port).
The `Request*` and `Job*` families are SYNONYMS — exactly the same handler. Stylistic choice for code style.
RequestText/RequestBlob are synchronous; BeginTextRequest/BeginBlobRequest are asynchronous.
WaitComplete takes a requestId (NOT listenerId), and does NOT destroy the request.
TakeTextResponse and TakeBlob destroy the request; the requestId is invalid afterward.
EnableTls is idempotent — calling it again with the same files just returns 0.
RouteClose and Close map to the same handler: close the listener, return the canceled queued requests count.
RouteCloseAccepted is a LATCH — once it's 1 it never resets.
An error (stopping the script) is caused by a wrong argument count or type, an out-of-range timeoutMs, an invalid listenerId/requestId, a closed plugin, TCP connect failure, and embedded NUL byte in a text response.
How to read the signatures
callbackName is the script-level callback name (`callback i32 NAME(ref blob $X)`). timeoutMs is the callback run-time (Open) or request I/O (Request*, Begin*) max in ms; -1 = infinite. listenerId is the positive id given by Open. requestId is the positive id given by Begin*. metaKey is one of the asyncmeta.RouteMeta* or JobMeta* values. The type after the -> arrow is the return type.
Lifecycle and TLS
Opening the listener on a loopback TCP port, optional TLS activation, and plugin shutdown.
tcpserver.Capabilities
tcpserver.Capabilities() -> intReturns the plugin's capability bitmask matching the asyncmeta constants.
This function takes no arguments.
asyncmeta.ProfileRouteService (=4095): every asyncmeta bit (the entire vocabulary supported).
printf("%d\n", tcpserver.Capabilities()); printf("%d\n", (tcpserver.Capabilities() == asyncmeta.ProfileRouteService()));
4095 1
For backend capability checking with the Supports* helpers. tcp_server supports the FULL vocabulary (like bus) — the Supports* helpers return 1 for everything.
tcpserver.Open
tcpserver.Open(callbackName, timeoutMs) -> intOpens a TCP listener on loopback IP and dispatches every incoming connection to the callback via a background thread.
| Parameter | Type | Description |
|---|---|---|
| callbackName | string | The script-level callback name to call. Signature: `callback i32 NAME(ref blob $X)`. |
| timeoutMs | int | The callback run-time limit in ms. -1 = infinite; otherwise 0..2147483647. |
The listenerId (positive i64). A bad argument, bind failure, or thread startup failure raises a runtime error.
callback i32 Echo(ref blob $X) { return(1); } i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", ($lid > 0));
1
For starting a local TCP service (debug server, local RPC, tested protocol). The port is OS-picked; query it with Port($lid). The callback receives RAW TCP bytes — any text or binary protocol is freely implementable.
tcpserver.SetClientCa
tcpserver.SetClientCa(listenerId, caFile, mode) -> 0
Turns on mTLS for this listener: requires (mode 2) or accepts (mode 1) a client certificate that chains to caFile on every TLS connection it accepts. The listener's mTLS shape is therefore Open + SetClientCa + EnableTls. With mTLS on, the 4-parameter callback form receives the verified client certificate's subject.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id returned by Open. |
| caFile | string | Path to the CA bundle (PEM) that client certificates are verified against. |
| mode | int | 1 = optional (accept a certificate if offered), 2 = required (the handshake fails without a valid one). |
Returns 0 on success; raises a runtime error on a bad argument, an unknown listener, a missing mTLS-capable TLS provider, or when TLS is already enabled.
Call it BEFORE EnableTls / OpenTls. Calling it afterwards is a runtime error by design — otherwise connections accepted in between would run unverified, leaving an unchecked window.
tcpserver.EnableTls
tcpserver.EnableTls(listenerId, certFile, keyFile) -> intActivates TLS on the listener: every subsequent connection is wrapped in TLS by the tls plugin's provider.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
| certFile | string | The server certificate path (PEM). |
| keyFile | string | The private key path (PEM). |
0 (success). Idempotent: calling it again with the same files just returns 0. If TLS is not supported on the platform, a clean runtime error.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); // tcpserver.EnableTls($lid, "server.crt", "server.key"); print.Line($"ok");
ok
For a secure local service with a TLS layer. The listener itself does not link OpenSSL — it relies on the tls plugin's provider at runtime. The probe didn't activate TLS (we only document the verb's presence).
tcpserver.OpenCount
tcpserver.OpenCount() -> intReturns the count of currently open listeners (plugin-wide, global view).
This function takes no arguments.
The number of open listeners (0 if none).
printf("%d\n", tcpserver.OpenCount()); i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.OpenCount());
0 1
For resource control and diagnostics. If it does not return to 0 after Close calls, that signals a resource leak.
tcpserver.Close
tcpserver.Close(listenerId) -> intCloses the given listener: stops the accept thread, shuts down the socket, releases resources.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id from Open. |
The count of queued requests canceled by the close. Typically 0 (clean close with nothing pending).
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Close($lid));
0
For clean listener shutdown. Close waits for in-progress callbacks to finish. If you called RouteClose first, Close returns 0 (listener no longer active).
Listener-level state
Current state of the listener: bound port, “open/closed” state, callback name, timeout, full diagnostic text. Route* names are equivalent to the State*/Info names.
tcpserver.Port
tcpserver.Port(listenerId) -> intReturns the listener's actual, bound TCP port.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The port (1..65535).
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", (tcpserver.Port($lid) > 0));
1
For querying the OS-picked port. RequestText/Blob and BeginTextRequest/BeginBlobRequest use this port automatically via the listenerId.
tcpserver.State
tcpserver.State(listenerId) -> intReturns the listener's state using the asyncmeta route-state constants.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
asyncmeta.RouteStateOpen (=1) or RouteStateClosed (=0).
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.State($lid));
1
For programmatic state checking. Shared vocabulary with the callback_* family.
tcpserver.RouteState
tcpserver.RouteState(listenerId) -> intSynonym of State — under the asyncmeta vocabulary.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
asyncmeta.RouteStateOpen (=1) or RouteStateClosed (=0).
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteState($lid));
1
For asyncmeta-compatible code.
tcpserver.StateText
tcpserver.StateText(listenerId) -> string
The state as text.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
“open” or “closed”.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); print.Line(tcpserver.StateText($lid));
open
For logging or UI display.
tcpserver.RouteStateText
tcpserver.RouteStateText(listenerId) -> string
Synonym of StateText.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
“open” or “closed”.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); print.Line(tcpserver.RouteStateText($lid));
open
For logging, under the asyncmeta vocabulary.
tcpserver.RouteCallback
tcpserver.RouteCallback(listenerId) -> string
Returns the callback name passed at Open.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The callback name as text.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); print.Line(tcpserver.RouteCallback($lid));
Echo
For reflective queries and diagnostics.
tcpserver.RouteTimeout
tcpserver.RouteTimeout(listenerId) -> intReturns the timeout value passed at Open, in ms.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The timeoutMs value; -1 for infinite.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteTimeout($lid));
5000
Reflective query of listener configuration.
tcpserver.RouteKnown
tcpserver.RouteKnown(listenerId) -> intTells whether the listenerId is known to the plugin.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
1 if known (open); 0 if not.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteKnown($lid));
1
Defensive check.
tcpserver.RouteMeta
tcpserver.RouteMeta(listenerId, metaKey) -> intReturns a specific meta field of the listener by asyncmeta.RouteMeta* keys.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
| metaKey | int | An asyncmeta.RouteMeta* value (State, Timeout, Pending, Queued, Running, Drained, Known, CloseAccepted, ClosedQueued). |
The value for the field.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteMeta($lid, asyncmeta.RouteMetaState())); printf("%d\n", tcpserver.RouteMeta($lid, asyncmeta.RouteMetaDrained()));
1 1
Uniform asyncmeta query — for portable multi-backend code.
tcpserver.Info
tcpserver.Info(listenerId) -> string
Returns the listener's aggregated key=value diagnostic state as a string.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
A space-separated key=value list.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", (str.Len(tcpserver.Info($lid)) > 0));
1
Detailed diagnostics in a single call.
tcpserver.RouteInfo
tcpserver.RouteInfo(listenerId) -> string
Synonym of Info — key=value diagnostic state.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
A space-separated key=value list.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", (str.Len(tcpserver.RouteInfo($lid)) > 0));
1
For logging, under the asyncmeta vocabulary.
Listener-level traffic
Listener traffic metrics: active, pending, queued, running connections; drain state and blocking wait. Route* names are synonyms here too.
tcpserver.Pending
tcpserver.Pending(listenerId) -> intReturns the number of pending (not yet started) requests.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of pending requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Pending($lid));
0
Early signal of congestion.
tcpserver.RoutePending
tcpserver.RoutePending(listenerId) -> intSynonym of Pending.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of pending requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RoutePending($lid));
0
Under the asyncmeta vocabulary.
tcpserver.Queued
tcpserver.Queued(listenerId) -> intReturns the number of queued requests.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of queued requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Queued($lid));
0
For monitoring the internal queue length.
tcpserver.RouteQueued
tcpserver.RouteQueued(listenerId) -> intSynonym of Queued.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of queued requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteQueued($lid));
0
Under the asyncmeta vocabulary.
tcpserver.Running
tcpserver.Running(listenerId) -> intReturns the number of currently running requests.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of running requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Running($lid));
0
For monitoring parallelism.
tcpserver.RouteRunning
tcpserver.RouteRunning(listenerId) -> intSynonym of Running.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of running requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteRunning($lid));
0
Under the asyncmeta vocabulary.
tcpserver.Active
tcpserver.Active(listenerId) -> intReturns the number of currently active connections.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The number of active connections.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Active($lid));
0
For monitoring current server load.
tcpserver.Drained
tcpserver.Drained(listenerId) -> intTells whether the listener is drained (pending = queued = running = active = 0).
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
1 if drained; 0 if something is in progress.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.Drained($lid));
1
Before clean shutdown: ensure Drained=1 before calling Close.
tcpserver.RouteDrained
tcpserver.RouteDrained(listenerId) -> intSynonym of Drained.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
1 if drained; 0 if not.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteDrained($lid));
1
Under the asyncmeta vocabulary.
tcpserver.WaitDrained
tcpserver.WaitDrained(listenerId, timeoutMs) -> intBlocks until the listener is drained, or timeoutMs expires.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
| timeoutMs | int | Max wait in ms; -1 = infinite. |
1 if drain occurred before timeoutMs expired; 0 if it expired.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.WaitDrained($lid, 50));
1
In the two-step shutdown pattern: AFTER RouteClose, wait for drain.
tcpserver.WaitRouteDrained
tcpserver.WaitRouteDrained(listenerId, timeoutMs) -> intSynonym of WaitDrained.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
| timeoutMs | int | Max wait in ms. |
1 if drain occurred; 0 if expired.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.WaitRouteDrained($lid, 50));
1
Under the asyncmeta vocabulary.
Request sending (client side)
Sending a TCP request to a listener's port (typically to test your own listener). Request* is synchronous, Begin* is asynchronous.
tcpserver.RequestText
tcpserver.RequestText(listenerId, text, timeoutMs) -> string
Sends a synchronous TCP request to the listener's port and returns the response text.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The target listener id. |
| text | string | The text to send (UTF-8 or ASCII; the response MUST NOT contain NUL bytes). |
| timeoutMs | int | Max time for the entire connection (connect + send + receive) in ms. |
The response text. If the response contains a NUL byte, a runtime error.
i64 $lid; string $resp[64]; $lid = tcpserver.Open("Echo", 5000); $resp = tcpserver.RequestText($lid, "PING", 5000); printf("%d\n", str.Len($resp));
0
For testing your own callback: send a specific request and check the response. A simple, synchronous pattern — if you need async, use BeginTextRequest.
tcpserver.RequestBlob
tcpserver.RequestBlob(listenerId, request, timeoutMs) -> blob
Sends a synchronous TCP request with a blob to the listener's port and returns the response blob.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The target listener id. |
| request | blob | The binary data to send. |
| timeoutMs | int | Max time for the entire connection in ms. |
The response blob (the callback-modified bytes). May be empty if the callback didn't write anything.
i64 $lid; blob $req[3]; blob $resp[16]; $req[0] = 65; $req[1] = 66; $req[2] = 67; $lid = tcpserver.Open("Echo", 5000); $resp = tcpserver.RequestBlob($lid, $req, 5000); printf("%d\n", $resp.Length);
0
For binary protocols. NUL bytes are allowed (only RequestText restricts them).
tcpserver.BeginTextRequest
tcpserver.BeginTextRequest(listenerId, text, timeoutMs) ->
intStarts an asynchronous TCP request: the background thread connects, sends, and waits — the call returns immediately with a requestId.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The target listener id. |
| text | string | The text to send. |
| timeoutMs | int | Max I/O time in ms. |
The requestId (positive i64).
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", ($jobId > 0));
1
For an async testing pattern: start a request, do other things, then WaitComplete + TakeTextResponse.
tcpserver.BeginBlobRequest
tcpserver.BeginBlobRequest(listenerId, request, timeoutMs) ->
intStarts an asynchronous TCP request with a blob payload.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The target listener id. |
| request | blob | The binary data to send. |
| timeoutMs | int | Max I/O time in ms. |
The requestId (positive i64).
i64 $lid; i64 $jobId; blob $req[3]; $req[0] = 65; $req[1] = 66; $req[2] = 67; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginBlobRequest($lid, $req, 5000); printf("%d\n", ($jobId > 0));
1
For async binary requests. Take the response with TakeBlob after WaitComplete.
Wait and response take
Waiting for an async request to complete and taking the response (text or blob).
tcpserver.WaitComplete
tcpserver.WaitComplete(requestId, timeoutMs) -> intBlocks until the request completes, but does NOT destroy it — status and result are queryable afterward.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId from Begin*. |
| timeoutMs | int | Max wait in ms; -1 = infinite. |
1 if the request completed; 0 if the timeout expired.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", tcpserver.WaitComplete($jobId, 5000));
1
First step of the async two-step pattern: wait for completion, verify success via Status / RequestStatus, then TakeTextResponse / TakeBlob the response.
tcpserver.TakeTextResponse
tcpserver.TakeTextResponse(requestId) -> string
Returns the text response of a completed request, and destroys the request.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The response text. If the request has not completed, a runtime error. If it contains a NUL byte, a runtime error.
i64 $lid; i64 $jobId; string $resp[64]; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); $resp = tcpserver.TakeTextResponse($jobId); printf("%d\n", str.Len($resp));
0
For a text response. If the callback didn't write anything, the response is an empty string. For binary, use TakeBlob.
tcpserver.TakeBlob
tcpserver.TakeBlob(requestId) -> blob
Returns the blob response of a completed request, and destroys the request.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The response blob.
i64 $lid; i64 $jobId; blob $req[3]; blob $resp[16]; $req[0] = 65; $req[1] = 66; $req[2] = 67; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginBlobRequest($lid, $req, 5000); tcpserver.WaitComplete($jobId, 5000); $resp = tcpserver.TakeBlob($jobId); printf("%d\n", $resp.Length);
0
For a binary response (may contain NUL bytes). The callback may modify the request bytes in place — these become the response.
Request-level state and metadata
Current state (phase, status, error) and metadata of an async request. Request* and Job* names are SYNONYMS — exactly the same handler, just stylistically interchangeable.
tcpserver.RequestPhase
tcpserver.RequestPhase(requestId) -> intReturns the request phase from the asyncmeta.Phase* constants.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
asyncmeta.PhaseQueued (=0), PhaseRunning (=1), or PhaseCompleted (=2).
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.RequestPhase($jobId));
2
For programmatic phase checks. Synonym of Phase.
tcpserver.Phase
tcpserver.Phase(requestId) -> intSynonym of RequestPhase — under the asyncmeta vocabulary.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
asyncmeta.PhaseQueued / PhaseRunning / PhaseCompleted.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.Phase($jobId));
2
For asyncmeta-compatible code.
tcpserver.RequestPhaseText
tcpserver.RequestPhaseText(requestId) -> string
The phase as text.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
“queued”, “running”, or “completed”.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); print.Line(tcpserver.RequestPhaseText($jobId));
completed
For logging or UI display.
tcpserver.PhaseText
tcpserver.PhaseText(requestId) -> string
Synonym of RequestPhaseText.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
“queued”, “running”, or “completed”.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); print.Line(tcpserver.PhaseText($jobId));
completed
Under the asyncmeta vocabulary.
tcpserver.RequestStatus
tcpserver.RequestStatus(requestId) -> intReturns the request status: 0 = success, non-zero = error.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
0 if successful; non-zero is the error code.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.RequestStatus($jobId));
0
Worth checking first after WaitComplete.
tcpserver.Status
tcpserver.Status(requestId) -> intSynonym of RequestStatus.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
0 or error code.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.Status($jobId));
0
Under the asyncmeta vocabulary.
tcpserver.RequestError
tcpserver.RequestError(requestId) -> string
Returns the request error message (empty on success).
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The error message; empty string on success.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", str.Len(tcpserver.RequestError($jobId)));
0
If Status is non-0, check this for the cause.
tcpserver.ErrorText
tcpserver.ErrorText(requestId) -> string
Synonym of RequestError.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The error message.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", str.Len(tcpserver.ErrorText($jobId)));
0
Under the asyncmeta vocabulary.
tcpserver.RequestStatusText
tcpserver.RequestStatusText(requestId) -> string
The textual counterpart of the status.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
“success” on a successful request, other text depending on the error type.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); print.Line(tcpserver.RequestStatusText($jobId));
success
For logging or user feedback.
tcpserver.StatusText
tcpserver.StatusText(requestId) -> string
Synonym of RequestStatusText.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
“success” or error text.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); print.Line(tcpserver.StatusText($jobId));
success
Under the asyncmeta vocabulary.
tcpserver.RequestInfo
tcpserver.RequestInfo(requestId) -> string
Returns the request state as a key=value diagnostic string.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
A space-separated key=value list.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", (str.Len(tcpserver.RequestInfo($jobId)) > 0));
1
Detailed request diagnostics in a single call.
tcpserver.JobInfo
tcpserver.JobInfo(requestId) -> string
Synonym of RequestInfo.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
A key=value diagnostic string.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", (str.Len(tcpserver.JobInfo($jobId)) > 0));
1
Under the asyncmeta vocabulary.
tcpserver.RequestRoute
tcpserver.RequestRoute(requestId) -> intReturns the id of the listener the request belongs to.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The listenerId.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", (tcpserver.RequestRoute($jobId) == $lid));
1
Reflective query of the request-owner relation.
tcpserver.JobRoute
tcpserver.JobRoute(requestId) -> intSynonym of RequestRoute.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The listenerId.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", (tcpserver.JobRoute($jobId) == $lid));
1
Under the asyncmeta vocabulary.
tcpserver.RequestTimeout
tcpserver.RequestTimeout(requestId) -> intReturns the request's configured timeout in ms (from Begin*).
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The timeoutMs value.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", tcpserver.RequestTimeout($jobId));
5000
For logging or configuration checks.
tcpserver.JobTimeout
tcpserver.JobTimeout(requestId) -> intSynonym of RequestTimeout.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The timeoutMs value.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", tcpserver.JobTimeout($jobId));
5000
Under the asyncmeta vocabulary.
tcpserver.RequestKnown
tcpserver.RequestKnown(requestId) -> intTells whether the requestId is known to the plugin.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
1 if known; 0 if not (e.g. already destroyed by Take).
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", tcpserver.RequestKnown($jobId));
1
Defensive check.
tcpserver.JobKnown
tcpserver.JobKnown(requestId) -> intSynonym of RequestKnown.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
1 if known; 0 if not.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); printf("%d\n", tcpserver.JobKnown($jobId));
1
Under the asyncmeta vocabulary.
tcpserver.RequestMeta
tcpserver.RequestMeta(requestId, metaKey) -> intReturns a meta field of the request by asyncmeta.JobMeta* keys.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
| metaKey | int | An asyncmeta.JobMeta* value (Phase, Status, Route, Timeout, Known, HasBlobResult, BlobResultSize). |
The value for the field.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.RequestMeta($jobId, asyncmeta.JobMetaPhase()));
2
Uniform asyncmeta query on the request state.
tcpserver.JobMeta
tcpserver.JobMeta(requestId, metaKey) -> intSynonym of RequestMeta.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
| metaKey | int | An asyncmeta.JobMeta* value. |
The value for the field.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.JobMeta($jobId, asyncmeta.JobMetaPhase()));
2
Under the asyncmeta vocabulary — for portable multi-backend code.
tcpserver.RequestHasBlobResult
tcpserver.RequestHasBlobResult(requestId) -> intTells whether the request has a blob result.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
1 if yes; 0 if no.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.RequestHasBlobResult($jobId));
1
Take precondition: if 0, TakeBlob raises a runtime error.
tcpserver.JobHasBlobResult
tcpserver.JobHasBlobResult(requestId) -> intSynonym of RequestHasBlobResult.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
1 if yes; 0 if no.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.JobHasBlobResult($jobId));
1
Under the asyncmeta vocabulary.
tcpserver.RequestBlobResultSize
tcpserver.RequestBlobResultSize(requestId) -> intReturns the response blob size in bytes.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The blob size in bytes.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.RequestBlobResultSize($jobId));
0
For memory pre-allocation: you know how big a blob to allocate for TakeBlob.
tcpserver.JobBlobResultSize
tcpserver.JobBlobResultSize(requestId) -> intSynonym of RequestBlobResultSize.
| Parameter | Type | Description |
|---|---|---|
| requestId | int | The requestId. |
The blob size in bytes.
i64 $lid; i64 $jobId; $lid = tcpserver.Open("Echo", 5000); $jobId = tcpserver.BeginTextRequest($lid, "HELLO", 5000); tcpserver.WaitComplete($jobId, 5000); printf("%d\n", tcpserver.JobBlobResultSize($jobId));
0
Under the asyncmeta vocabulary.
Listener shutdown
Diagnostics of the listener (route) shutdown process and explicit shutdown. RouteClose and Close do the same thing under different names.
tcpserver.RouteCloseAccepted
tcpserver.RouteCloseAccepted(listenerId) -> intTells whether a close request has been accepted on the listener.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
1 if yes; 0 if no RouteClose / Close has happened yet.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); tcpserver.RouteClose($lid); printf("%d\n", tcpserver.RouteCloseAccepted($lid));
1
LATCH: once 1, it never resets. Useful for diagnostics.
tcpserver.RouteClosedQueued
tcpserver.RouteClosedQueued(listenerId) -> intReturns the count of queued requests canceled by the most recent RouteClose / Close.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The count of canceled queued requests.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); tcpserver.RouteClose($lid); printf("%d\n", tcpserver.RouteClosedQueued($lid));
0
For diagnostics: how many requests were lost during shutdown.
tcpserver.RouteClose
tcpserver.RouteClose(listenerId) -> intSynonym of Close — exact same handler.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
The count of canceled queued requests (0 = clean close).
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", tcpserver.RouteClose($lid));
0
Under the asyncmeta vocabulary. Equivalent to Close — stylistic choice.
tcpserver.RouteCloseInfo
tcpserver.RouteCloseInfo(listenerId) -> string
Returns the listener shutdown process as a key=value diagnostic string.
| Parameter | Type | Description |
|---|---|---|
| listenerId | int | The listener id. |
A space-separated key=value list.
i64 $lid; $lid = tcpserver.Open("Echo", 5000); printf("%d\n", (str.Len(tcpserver.RouteCloseInfo($lid)) > 0));
1
Detailed shutdown diagnostics in a single call.
Practical notes
What the tcpserver plugin is good for
Starting a local TCP service on a loopback port (debug server, local RPC, tested protocol).
Event-driven processing: every incoming TCP connection is a callback invocation; the callback gets RAW bytes and can implement any protocol.
Built-in client side for testing your own server: `RequestText`/`RequestBlob` are synchronous, `BeginTextRequest`/`BeginBlobRequest` are asynchronous — both open a loopback TCP connection.
Optional TLS layer with EnableTls: incoming connections are TLS-wrapped, the callback receives the decoded bytes unchanged.
asyncmeta compatibility with the full vocabulary: Phase, Status, JobMeta, RouteMeta, WaitDrained, RouteClose — every bit supported (Capabilities = 4095).
Server + client in one — the testing pattern
A key feature of the plugin is that a complete TCP exchange is reachable from one script: Open opens a server, and RequestText immediately calls the server itself. This is ideal for testing a callback implementation: you can invoke your own logic through a real TCP channel, ensuring the endpoints communicate properly. A minimal test: open a listener → send RequestText → check the response → close.
Request* vs Job* — synonym pairs
Every job-level (= queryable per async request) function has two names: the native `Request*` (paints the HTTP/RPC request image) and the asyncmeta-vocabulary `Job*`. The two forms are EXACTLY the same handler — pick stylistically. If your code works with other asyncmeta-compatible backends, `Job*` is more consistent; if only tcpserver, `Request*` is more expressive.
Sync vs async request
`RequestText`/`RequestBlob` are SYNCHRONOUS: the call blocks until the TCP connection + send + receive finishes. Simple, but the script does nothing else meanwhile. `BeginTextRequest`/`BeginBlobRequest` are ASYNCHRONOUS: they return immediately with a requestId, the background thread handles I/O. Then WaitComplete + TakeTextResponse / TakeBlob finish and collect. For high-throughput or parallel testing, the async form is recommended.
TLS activation
`EnableTls(listenerId, certFile, keyFile)` is IDEMPOTENT: calling it again with the same files just returns 0. The TLS layer runs through the tls plugin's provider — resolved at runtime, so the tcpserver plugin itself does not link OpenSSL. If the platform doesn't support TLS (e.g. a Windows pre-bundle build), EnableTls returns a clean error and the listener keeps serving plaintext.
Callback signature
The callback signature is `callback i32 NAME(ref blob $Request)`. The incoming `$Request` blob contains the RAW bytes received over the TCP connection — the plugin does not interpret HTTP, does not look for protocol framing. The callback may modify the blob bytes in place, and the modified content becomes the TCP response. If the callback writes nothing, the response is empty (as in the probe — the Echo callback returns 1 without modifying the blob).
Listener shutdown
`RouteClose(listenerId)` and `Close(listenerId)` are the SAME handler — two names, identical behavior. The return value is NOT a 1/0 success flag, but the COUNT of queued requests canceled by the close. Typically 0 (clean close with no pending requests). The two-step shutdown pattern: RouteClose → WaitDrained → forced cleanup (if needed). A single Close performs all steps.
Error handling
An invalid listenerId, requestId, wrong argument count or type, out-of-range timeoutMs, touching an already-closed listener or plugin are reported by the runtime as a runtime error, and the script stops. A TCP connect failure (e.g. connecting to a closed listener's port) gives an error code in the Status field; `RequestError` returns the error text. `TakeTextResponse` raises a runtime error if the response contains an embedded NUL byte — use TakeBlob instead.