---
title: "stream"
description: "Named byte streams by URL scheme: TCP, tty, Bluetooth Low Energy and loopback transports, dialled out or listened…"
section: "API Reference"
slug: "api-stream"
canonical: "https://origozero.ai/docs/api-stream"
updated: "2026-10-20T13:15:37.034243770+00:00"
tags: ["api", "reference"]
---

# stream

Named byte streams by URL scheme — TCP, tty, Bluetooth Low Energy and loopback transports, dialled out or listened for, with a common read/write/status/close surface over every one of them.

```lua
require("@builtin::modules.api.engine.stream") -- stream (also available as global 'stream')
```

The `stream` namespace: 36 functions.

## globals/stream/accept {#globals-stream-accept}

```lua
stream.accept(listener: string) -> string?
```

Take the connection that has waited longest on the listener, as a
stream handle that reads, writes, and closes exactly like one
`stream.open` returned. Returns nil when nothing is waiting, so call
it in a loop each tick to take every peer that arrived.
`stream.listenerStatus(listener).pending` is how many are still
waiting.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

**Returns** `string?` — The connection's stream handle, or nil when none is waiting.

```lua
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end
```

## globals/stream/close {#globals-stream-close}

```lua
stream.close(handle: string) -> boolean
```

Finish whatever `handle` names — a stream or a listener — and
drop it from the registry.

Closing a stream refuses every later write and carries the bytes
already queued to the peer before the connection ends, so a write and
a close in the same tick deliver — the shape a request answered with
one response has. A peer that has stopped reading altogether holds
that finish for thirty seconds; past that the connection ends and
what is still queued ends with it, so a caller that must know its
bytes went out watches `stream.status(handle).pending` reach zero
before it closes.

Closing a listener stops it answering new peers and closes the
connections nobody took; the connections `stream.accept` already
handed over keep running until they are closed themselves.

**Parameters**

- `handle` `string` — Stream handle from stream.open or stream.accept, or listener handle from stream.listen.

**Returns** `boolean` — True if a stream or listener was closed, false if handle already named none.

```lua
stream.write(peer, response); stream.close(peer)
```

## globals/stream/listen {#globals-stream-listen}

```lua
stream.listen(url: string, opts: StreamListenOpts?) -> PromiseHandle
```

Hold the address `url` names and answer the peers that dial it —
the other direction from `stream.open`, for when the thing you are
talking to starts the conversation and restarts on its own schedule.
Returns a promise handle: `task.await()` it to get the listener
handle once the address is held, or it raises the reason a malformed
url, a scheme that cannot listen, or a failed bind was refused with.
Take the connections with `stream.accept`.

**The host in the url is the interface bound, and the whole of what
decides who can reach it.** `tcp://127.0.0.1:9000` answers only
programs on this same machine. `tcp://0.0.0.0:9000` answers any host
that can route to this machine on that port — every device on the
wifi, and anything beyond it the network lets through. Write the one
you mean; there is no default, and `stream.listenerStatus` reports
which of the two you got. A port of `0` asks the operating system to
choose one, which that same status then reports.

`opts.inboundCapacity` and `opts.outboundCapacity` bound each
answered connection (65536 bytes each by default);
`opts.backlog` bounds the connections held for `stream.accept`
before the listener stops taking them from the operating system,
which leaves the rest queued in the kernel rather than answered and
forgotten (16 by default, and at least 1).

The listener belongs to the chunk that opened it — the chunk whose
own code called `stream.listen`, which is the module holding that
line even when something else called into it. When that chunk runs
again — a module hot-reload, a cleared require cache — the listener
and the connections it answered are closed, and the new run binds the
address for itself. Peers see the connection close and dial again.
`stream.listeners()` names that chunk as each entry's `owner`.

**Parameters**

- `url` `string` — Listen URL — scheme://host:port.
- `opts` `StreamListenOpts` _(optional)_ — Per-connection capacities and the accept backlog (optional).

**Returns** `PromiseHandle` — Promise handle for task.await().

```lua
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)
```

## globals/stream/listenerStatus {#globals-stream-listenerstatus}

```lua
stream.listenerStatus(listener: string) -> ListenerStatus?
```

Report what the listener holds and has handed over. `address` is
the address the operating system resolved the bind to, port
included — the one to hand a peer. `reach` says who can connect to
it: `"thisMachine"` when it is a loopback address and only programs
on this machine can, `"network"` when any host that can route here
can. `accepted` counts the connections `stream.accept` handed over,
`pending` the ones still waiting, and `capacity` the value `pending`
may reach before the listener stops taking connections from the
operating system. nil when handle names no open listener.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

**Returns** `ListenerStatus?` — Listener status, or nil when handle names no open listener.

```lua
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)
```

## globals/stream/listeners {#globals-stream-listeners}

```lua
stream.listeners() -> { OpenListener }
```

Every listener this engine currently holds an address for, in the
order they were opened. Each entry is what `stream.listenerStatus`
reports about it, plus the `handle` it is addressed by and the `owner`
chunk its life follows.

This is how an address is reached again once nothing holds its handle:
filter on `address` for the port you want and close the entry by its
`handle`, rather than guessing at handles.

**Returns** `{ OpenListener }` — An array of open listeners.

```lua
for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end
```

## globals/stream/open {#globals-stream-open}

```lua
stream.open(url: string, opts: StreamOpenOpts?) -> PromiseHandle
```

Open a byte stream at url (`scheme://target[?k=v]`). `loopback`
carries written bytes back out of the same stream and works on
every platform; `tcp` dials `host:port`; `tty` opens a serial
device node — `/dev/ttyACM0` or `/dev/ttyUSB0` for a USB CDC board
such as an ESP32, `/dev/rfcomm0` for a Bluetooth controller paired
over classic SPP (both present as a tty on Linux, so one transport
serves either peer), `COM5` on Windows. `tty` query parameters:
`baud` (default 115200), `dataBits` (5-8, default 8), `parity`
(`none` | `odd` | `even`, default `none`), `stopBits` (1 or 2,
default 1).

`ble` connects to a Bluetooth Low Energy device over GATT, on a
desktop engine and in a browser alike — the wireless transport a
web world reaches a device through:
`ble://<device>?service=<uuid>&write=<uuid>&notify=<uuid>`. The
device is the name it advertises, `*` any device offering the
service, a trailing `*` a name prefix (`Paw*`). `write` is the
characteristic this engine writes to and `notify` the one it
subscribes to, which on a Nordic UART peripheral are that
peripheral's RX and TX; a module with one bidirectional
characteristic names it for both. UUIDs may be 16-bit (`ffe0`),
32-bit, or full. Optional: `chunk` (bytes per packet, 1-512 —
otherwise what the connection carries), `writeMode`
(`withResponse` | `withoutResponse`, default `withResponse`),
`timeout` (seconds to find and connect to the device, default
15).

`opts` bounds the stream's undrained inbound buffer
and in-flight outbound bytes (default 65536 each). Returns a
promise handle: `task.await()` it
to get the stream handle once the transport is open, or it raises
the reason a malformed url, an unknown or unsupported scheme, or a
failed connect was refused with. A `ble` stream resolves as soon as
it exists and reports the rest as state — watch
`stream.status(handle).state` go `opening`, `permissionPending`
while the browser asks the person at the machine to pick a device,
then `open`; writes made meanwhile are queued and go out when it
connects. Check `stream.transports()` first
to tell a mistyped scheme from one this build does not carry.

**Parameters**

- `url` `string` — Stream URL — scheme://target[?k=v&k=v].
- `opts` `StreamOpenOpts` _(optional)_ — Buffer capacities (optional).

**Returns** `PromiseHandle` — Promise handle for task.await().

```lua
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1&notify=ffe1"))
```

## globals/stream/read {#globals-stream-read}

```lua
stream.read(handle: string, max: number?) -> string
```

Drain up to max buffered inbound bytes from the stream.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `max` `number` _(optional)_ — Maximum bytes to drain (optional). Omit to drain everything buffered.

**Returns** `string` — Drained bytes, byte-safe. "" when none buffered or handle names no open stream.

```lua
local chunk = stream.read(handle)
```

## globals/stream/serialPorts {#globals-stream-serialports}

```lua
stream.serialPorts() -> SerialPorts
```

Every serial device this machine has, for picking the one to open.
`ports` is an array ordered by path. Each entry carries the `path` the
device is at (`/dev/ttyACM0` on Linux, `COM3` on Windows), the `url`
that opens it, the `kind` of bus it attaches by, and — for a USB
device — the `vendorId`, `productId`, `serialNumber`, `manufacturer`
and `product` it advertises.

A device's path moves with enumeration order: a board that came up at
`/dev/ttyACM0` is at `/dev/ttyACM1` once something else is plugged in
first, and moves across `COM3`-`COM5` on Windows. What the device
advertises holds still across those moves, so match on
`vendorId`/`productId` — or on `serialNumber` to tell two of the same
board apart — and open the `url` that entry carries, appending the
port settings `stream.open` documents.

Three answers are distinct. `supported` false with a `reason` means
this platform has no serial bus to enumerate at all. `error` set means
it has one and the operating system refused this enumeration, so a
later call may answer. An empty `ports` with neither means the machine
has no serial device attached, which is an ordinary result.

**Returns** `SerialPorts` — { supported, reason, error, ports } — the platform's answer, this enumeration's, and the devices it found.

```lua
for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end
```

## globals/stream/status {#globals-stream-status}

```lua
stream.status(handle: string) -> StreamStatus?
```

Report what the stream has carried and lost. `state` is where
the stream is in its life: `opening`, `permissionPending` while the
platform asks the person at the machine to allow the connection,
`open`, `denied` when that permission was refused, and `closed`
when it is finished. `pending` is bytes
accepted and not yet handed to the peer; `capacity` is the value
`pending` may reach before a write is refused. `error` holds the
most recent transport failure and the refusal a `denied` stream
carries, retained for the life of the
stream. nil when handle names no open stream.

**Parameters**

- `handle` `string`

**Returns** `StreamStatus?` — Stream status, or nil when handle names no open stream.

```lua
local s = stream.status(handle); print(s.pending, s.capacity)
```

## globals/stream/streams {#globals-stream-streams}

```lua
stream.streams() -> { OpenStream }
```

Every open stream, dialled or answered, in the order they were
opened. Each entry is what `stream.status` reports about it, plus the
`handle` it is addressed by and the `owner` chunk its life follows —
a connection `stream.accept` handed over carries the owner of the
listener that answered it.

**Returns** `{ OpenStream }` — An array of open streams.

```lua
for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end
```

## globals/stream/transports {#globals-stream-transports}

```lua
stream.transports() -> { [string]: TransportSupport }
```

Every stream scheme this build knows about — a capability
probe, in both directions. `supported` answers `stream.open` and
`listen` answers `stream.listen`, since a scheme can carry one and
not the other. Each reason is nil when its direction works,
otherwise it names why not: an unbuilt transport names its own
absence, a transport this platform lacks (`tcp` and `tty` on wasm;
`ble` in a browser without Web Bluetooth or with the radio off,
which the page itself answers) names that, and a loopback stream,
whose peer is itself, names that nothing dials it. A typo'd scheme
is absent from this table entirely, which is what tells it apart
from a real transport this build lacks.

**Returns** `{ [string]: TransportSupport }` — Map of scheme name to { supported, reason, listen, listenReason }.

```lua
local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end
```

## globals/stream/write {#globals-stream-write}

```lua
stream.write(handle: string, bytes: string) -> WriteOutcome
```

Queue bytes for the stream's peer. Never blocks. `"accepted"`
means the bytes were queued. `"full"` means the outbound queue has
no room right now — backpressure, not failure: the peer is alive
and draining slower than this call is producing, so a retry after
it catches up can succeed. Compare `pending` against `capacity` on
`stream.status()` to see it coming before a write is refused.
`"closed"` means the stream is finished, or handle names no open
stream — reopen to continue, retrying never succeeds. `"tooLarge"`
means bytes is bigger than the stream's whole outbound capacity, so
it can never fit at any queue depth — retrying the same write
returns this again.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `bytes` `string` — Bytes to queue, byte-safe.

**Returns** `WriteOutcome` — "accepted" | "full" | "closed" | "tooLarge"

```lua
local outcome = stream.write(handle, data)
```

## modules/stream/accept {#modules-stream-accept}

```lua
accept(listener: string): string?
```

Take the connection that has waited longest on the listener, as a
stream handle that reads, writes, and closes exactly like one
`stream.open` returned. Returns nil when nothing is waiting, so call
it in a loop each tick to take every peer that arrived.
`stream.listenerStatus(listener).pending` is how many are still
waiting.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

```lua
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end
```

## modules/stream/close {#modules-stream-close}

```lua
close(handle: string): boolean
```

Finish whatever `handle` names — a stream or a listener — and
drop it from the registry.

Closing a stream refuses every later write and carries the bytes
already queued to the peer before the connection ends, so a write and
a close in the same tick deliver — the shape a request answered with
one response has. A peer that has stopped reading altogether holds
that finish for thirty seconds; past that the connection ends and
what is still queued ends with it, so a caller that must know its
bytes went out watches `stream.status(handle).pending` reach zero
before it closes.

Closing a listener stops it answering new peers and closes the
connections nobody took; the connections `stream.accept` already
handed over keep running until they are closed themselves.

**Parameters**

- `handle` `string` — Stream handle from stream.open or stream.accept, or listener handle from stream.listen.

```lua
stream.write(peer, response); stream.close(peer)
```

## modules/stream/listen {#modules-stream-listen}

```lua
listen(url: string, opts: StreamListenOpts?): PromiseHandle
```

Hold the address `url` names and answer the peers that dial it —
the other direction from `stream.open`, for when the thing you are
talking to starts the conversation and restarts on its own schedule.
Returns a promise handle: `task.await()` it to get the listener
handle once the address is held, or it raises the reason a malformed
url, a scheme that cannot listen, or a failed bind was refused with.
Take the connections with `stream.accept`.

**The host in the url is the interface bound, and the whole of what
decides who can reach it.** `tcp://127.0.0.1:9000` answers only
programs on this same machine. `tcp://0.0.0.0:9000` answers any host
that can route to this machine on that port — every device on the
wifi, and anything beyond it the network lets through. Write the one
you mean; there is no default, and `stream.listenerStatus` reports
which of the two you got. A port of `0` asks the operating system to
choose one, which that same status then reports.

`opts.inboundCapacity` and `opts.outboundCapacity` bound each
answered connection (65536 bytes each by default);
`opts.backlog` bounds the connections held for `stream.accept`
before the listener stops taking them from the operating system,
which leaves the rest queued in the kernel rather than answered and
forgotten (16 by default, and at least 1).

The listener belongs to the chunk that opened it — the chunk whose
own code called `stream.listen`, which is the module holding that
line even when something else called into it. When that chunk runs
again — a module hot-reload, a cleared require cache — the listener
and the connections it answered are closed, and the new run binds the
address for itself. Peers see the connection close and dial again.
`stream.listeners()` names that chunk as each entry's `owner`.

**Parameters**

- `url` `string` — Listen URL — scheme://host:port.
- `opts` `StreamListenOpts?` _(optional)_ — Per-connection capacities and the accept backlog (optional).

```lua
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)
```

## modules/stream/listenerStatus {#modules-stream-listenerstatus}

```lua
listenerStatus(listener: string): ListenerStatus?
```

Report what the listener holds and has handed over. `address` is
the address the operating system resolved the bind to, port
included — the one to hand a peer. `reach` says who can connect to
it: `"thisMachine"` when it is a loopback address and only programs
on this machine can, `"network"` when any host that can route here
can. `accepted` counts the connections `stream.accept` handed over,
`pending` the ones still waiting, and `capacity` the value `pending`
may reach before the listener stops taking connections from the
operating system. nil when handle names no open listener.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

```lua
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)
```

## modules/stream/listeners {#modules-stream-listeners}

```lua
listeners(): { OpenListener }
```

Every listener this engine currently holds an address for, in the
order they were opened. Each entry is what `stream.listenerStatus`
reports about it, plus the `handle` it is addressed by and the `owner`
chunk its life follows.

This is how an address is reached again once nothing holds its handle:
filter on `address` for the port you want and close the entry by its
`handle`, rather than guessing at handles.

```lua
for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end
```

## modules/stream/open {#modules-stream-open}

```lua
open(url: string, opts: StreamOpenOpts?): PromiseHandle
```

Open a byte stream at url (`scheme://target[?k=v]`). `loopback`
carries written bytes back out of the same stream and works on
every platform; `tcp` dials `host:port`; `tty` opens a serial
device node — `/dev/ttyACM0` or `/dev/ttyUSB0` for a USB CDC board
such as an ESP32, `/dev/rfcomm0` for a Bluetooth controller paired
over classic SPP (both present as a tty on Linux, so one transport
serves either peer), `COM5` on Windows. `tty` query parameters:
`baud` (default 115200), `dataBits` (5-8, default 8), `parity`
(`none` | `odd` | `even`, default `none`), `stopBits` (1 or 2,
default 1).

`ble` connects to a Bluetooth Low Energy device over GATT, on a
desktop engine and in a browser alike — the wireless transport a
web world reaches a device through:
`ble://<device>?service=<uuid>&write=<uuid>&notify=<uuid>`. The
device is the name it advertises, `*` any device offering the
service, a trailing `*` a name prefix (`Paw*`). `write` is the
characteristic this engine writes to and `notify` the one it
subscribes to, which on a Nordic UART peripheral are that
peripheral's RX and TX; a module with one bidirectional
characteristic names it for both. UUIDs may be 16-bit (`ffe0`),
32-bit, or full. Optional: `chunk` (bytes per packet, 1-512 —
otherwise what the connection carries), `writeMode`
(`withResponse` | `withoutResponse`, default `withResponse`),
`timeout` (seconds to find and connect to the device, default
15).

`opts` bounds the stream's undrained inbound buffer
and in-flight outbound bytes (default 65536 each). Returns a
promise handle: `task.await()` it
to get the stream handle once the transport is open, or it raises
the reason a malformed url, an unknown or unsupported scheme, or a
failed connect was refused with. A `ble` stream resolves as soon as
it exists and reports the rest as state — watch
`stream.status(handle).state` go `opening`, `permissionPending`
while the browser asks the person at the machine to pick a device,
then `open`; writes made meanwhile are queued and go out when it
connects. Check `stream.transports()` first
to tell a mistyped scheme from one this build does not carry.

**Parameters**

- `url` `string` — Stream URL — scheme://target[?k=v&k=v].
- `opts` `StreamOpenOpts?` _(optional)_ — Buffer capacities (optional).

```lua
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1&notify=ffe1"))
```

## modules/stream/read {#modules-stream-read}

```lua
read(handle: string, max: number?): string
```

Drain up to max buffered inbound bytes from the stream.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `max` `number?` _(optional)_ — Maximum bytes to drain (optional). Omit to drain everything buffered.

```lua
local chunk = stream.read(handle)
```

## modules/stream/serialPorts {#modules-stream-serialports}

```lua
serialPorts(): SerialPorts
```

Every serial device this machine has, for picking the one to open.
`ports` is an array ordered by path. Each entry carries the `path` the
device is at (`/dev/ttyACM0` on Linux, `COM3` on Windows), the `url`
that opens it, the `kind` of bus it attaches by, and — for a USB
device — the `vendorId`, `productId`, `serialNumber`, `manufacturer`
and `product` it advertises.

A device's path moves with enumeration order: a board that came up at
`/dev/ttyACM0` is at `/dev/ttyACM1` once something else is plugged in
first, and moves across `COM3`-`COM5` on Windows. What the device
advertises holds still across those moves, so match on
`vendorId`/`productId` — or on `serialNumber` to tell two of the same
board apart — and open the `url` that entry carries, appending the
port settings `stream.open` documents.

Three answers are distinct. `supported` false with a `reason` means
this platform has no serial bus to enumerate at all. `error` set means
it has one and the operating system refused this enumeration, so a
later call may answer. An empty `ports` with neither means the machine
has no serial device attached, which is an ordinary result.

```lua
for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end
```

## modules/stream/status {#modules-stream-status}

```lua
status(handle: string): StreamStatus?
```

Report what the stream has carried and lost. `state` is where
the stream is in its life: `opening`, `permissionPending` while the
platform asks the person at the machine to allow the connection,
`open`, `denied` when that permission was refused, and `closed`
when it is finished. `pending` is bytes
accepted and not yet handed to the peer; `capacity` is the value
`pending` may reach before a write is refused. `error` holds the
most recent transport failure and the refusal a `denied` stream
carries, retained for the life of the
stream. nil when handle names no open stream.

**Parameters**

- `handle` `string`

```lua
local s = stream.status(handle); print(s.pending, s.capacity)
```

## modules/stream/streams {#modules-stream-streams}

```lua
streams(): { OpenStream }
```

Every open stream, dialled or answered, in the order they were
opened. Each entry is what `stream.status` reports about it, plus the
`handle` it is addressed by and the `owner` chunk its life follows —
a connection `stream.accept` handed over carries the owner of the
listener that answered it.

```lua
for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end
```

## modules/stream/transports {#modules-stream-transports}

```lua
transports(): { [string]: TransportSupport }
```

Every stream scheme this build knows about — a capability
probe, in both directions. `supported` answers `stream.open` and
`listen` answers `stream.listen`, since a scheme can carry one and
not the other. Each reason is nil when its direction works,
otherwise it names why not: an unbuilt transport names its own
absence, a transport this platform lacks (`tcp` and `tty` on wasm;
`ble` in a browser without Web Bluetooth or with the radio off,
which the page itself answers) names that, and a loopback stream,
whose peer is itself, names that nothing dials it. A typo'd scheme
is absent from this table entirely, which is what tells it apart
from a real transport this build lacks.

```lua
local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end
```

## modules/stream/write {#modules-stream-write}

```lua
write(handle: string, bytes: string): WriteOutcome
```

Queue bytes for the stream's peer. Never blocks. `"accepted"`
means the bytes were queued. `"full"` means the outbound queue has
no room right now — backpressure, not failure: the peer is alive
and draining slower than this call is producing, so a retry after
it catches up can succeed. Compare `pending` against `capacity` on
`stream.status()` to see it coming before a write is refused.
`"closed"` means the stream is finished, or handle names no open
stream — reopen to continue, retrying never succeeds. `"tooLarge"`
means bytes is bigger than the stream's whole outbound capacity, so
it can never fit at any queue depth — retrying the same write
returns this again.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `bytes` `string` — Bytes to queue, byte-safe.

```lua
local outcome = stream.write(handle, data)
```

## typed/builtin//modules/api/engine/stream/stream/accept {#typed-builtin-modules-api-engine-stream-stream-accept}

```lua
stream.accept(listener: string) -> string?
```

Take the connection that has waited longest on the listener, as a
stream handle that reads, writes, and closes exactly like one
`stream.open` returned. Returns nil when nothing is waiting, so call
it in a loop each tick to take every peer that arrived.
`stream.listenerStatus(listener).pending` is how many are still
waiting.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

**Returns** `string?` — The connection's stream handle, or nil when none is waiting.

```lua
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end
```

## typed/builtin//modules/api/engine/stream/stream/close {#typed-builtin-modules-api-engine-stream-stream-close}

```lua
stream.close(handle: string) -> boolean
```

Finish whatever `handle` names — a stream or a listener — and
drop it from the registry.

Closing a stream refuses every later write and carries the bytes
already queued to the peer before the connection ends, so a write and
a close in the same tick deliver — the shape a request answered with
one response has. A peer that has stopped reading altogether holds
that finish for thirty seconds; past that the connection ends and
what is still queued ends with it, so a caller that must know its
bytes went out watches `stream.status(handle).pending` reach zero
before it closes.

Closing a listener stops it answering new peers and closes the
connections nobody took; the connections `stream.accept` already
handed over keep running until they are closed themselves.

**Parameters**

- `handle` `string` — Stream handle from stream.open or stream.accept, or listener handle from stream.listen.

**Returns** `boolean` — True if a stream or listener was closed, false if handle already named none.

```lua
stream.write(peer, response); stream.close(peer)
```

## typed/builtin//modules/api/engine/stream/stream/listen {#typed-builtin-modules-api-engine-stream-stream-listen}

```lua
stream.listen(url: string, opts: StreamListenOpts?) -> PromiseHandle
```

Hold the address `url` names and answer the peers that dial it —
the other direction from `stream.open`, for when the thing you are
talking to starts the conversation and restarts on its own schedule.
Returns a promise handle: `task.await()` it to get the listener
handle once the address is held, or it raises the reason a malformed
url, a scheme that cannot listen, or a failed bind was refused with.
Take the connections with `stream.accept`.

**The host in the url is the interface bound, and the whole of what
decides who can reach it.** `tcp://127.0.0.1:9000` answers only
programs on this same machine. `tcp://0.0.0.0:9000` answers any host
that can route to this machine on that port — every device on the
wifi, and anything beyond it the network lets through. Write the one
you mean; there is no default, and `stream.listenerStatus` reports
which of the two you got. A port of `0` asks the operating system to
choose one, which that same status then reports.

`opts.inboundCapacity` and `opts.outboundCapacity` bound each
answered connection (65536 bytes each by default);
`opts.backlog` bounds the connections held for `stream.accept`
before the listener stops taking them from the operating system,
which leaves the rest queued in the kernel rather than answered and
forgotten (16 by default, and at least 1).

The listener belongs to the chunk that opened it — the chunk whose
own code called `stream.listen`, which is the module holding that
line even when something else called into it. When that chunk runs
again — a module hot-reload, a cleared require cache — the listener
and the connections it answered are closed, and the new run binds the
address for itself. Peers see the connection close and dial again.
`stream.listeners()` names that chunk as each entry's `owner`.

**Parameters**

- `url` `string` — Listen URL — scheme://host:port.
- `opts` `StreamListenOpts` _(optional)_ — Per-connection capacities and the accept backlog (optional).

**Returns** `PromiseHandle` — Promise handle for task.await().

```lua
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)
```

## typed/builtin//modules/api/engine/stream/stream/listenerStatus {#typed-builtin-modules-api-engine-stream-stream-listenerstatus}

```lua
stream.listenerStatus(listener: string) -> ListenerStatus?
```

Report what the listener holds and has handed over. `address` is
the address the operating system resolved the bind to, port
included — the one to hand a peer. `reach` says who can connect to
it: `"thisMachine"` when it is a loopback address and only programs
on this machine can, `"network"` when any host that can route here
can. `accepted` counts the connections `stream.accept` handed over,
`pending` the ones still waiting, and `capacity` the value `pending`
may reach before the listener stops taking connections from the
operating system. nil when handle names no open listener.

**Parameters**

- `listener` `string` — Listener handle from stream.listen.

**Returns** `ListenerStatus?` — Listener status, or nil when handle names no open listener.

```lua
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)
```

## typed/builtin//modules/api/engine/stream/stream/listeners {#typed-builtin-modules-api-engine-stream-stream-listeners}

```lua
stream.listeners() -> { OpenListener }
```

Every listener this engine currently holds an address for, in the
order they were opened. Each entry is what `stream.listenerStatus`
reports about it, plus the `handle` it is addressed by and the `owner`
chunk its life follows.

This is how an address is reached again once nothing holds its handle:
filter on `address` for the port you want and close the entry by its
`handle`, rather than guessing at handles.

**Returns** `{ OpenListener }` — An array of open listeners.

```lua
for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end
```

## typed/builtin//modules/api/engine/stream/stream/open {#typed-builtin-modules-api-engine-stream-stream-open}

```lua
stream.open(url: string, opts: StreamOpenOpts?) -> PromiseHandle
```

Open a byte stream at url (`scheme://target[?k=v]`). `loopback`
carries written bytes back out of the same stream and works on
every platform; `tcp` dials `host:port`; `tty` opens a serial
device node — `/dev/ttyACM0` or `/dev/ttyUSB0` for a USB CDC board
such as an ESP32, `/dev/rfcomm0` for a Bluetooth controller paired
over classic SPP (both present as a tty on Linux, so one transport
serves either peer), `COM5` on Windows. `tty` query parameters:
`baud` (default 115200), `dataBits` (5-8, default 8), `parity`
(`none` | `odd` | `even`, default `none`), `stopBits` (1 or 2,
default 1).

`ble` connects to a Bluetooth Low Energy device over GATT, on a
desktop engine and in a browser alike — the wireless transport a
web world reaches a device through:
`ble://<device>?service=<uuid>&write=<uuid>&notify=<uuid>`. The
device is the name it advertises, `*` any device offering the
service, a trailing `*` a name prefix (`Paw*`). `write` is the
characteristic this engine writes to and `notify` the one it
subscribes to, which on a Nordic UART peripheral are that
peripheral's RX and TX; a module with one bidirectional
characteristic names it for both. UUIDs may be 16-bit (`ffe0`),
32-bit, or full. Optional: `chunk` (bytes per packet, 1-512 —
otherwise what the connection carries), `writeMode`
(`withResponse` | `withoutResponse`, default `withResponse`),
`timeout` (seconds to find and connect to the device, default
15).

`opts` bounds the stream's undrained inbound buffer
and in-flight outbound bytes (default 65536 each). Returns a
promise handle: `task.await()` it
to get the stream handle once the transport is open, or it raises
the reason a malformed url, an unknown or unsupported scheme, or a
failed connect was refused with. A `ble` stream resolves as soon as
it exists and reports the rest as state — watch
`stream.status(handle).state` go `opening`, `permissionPending`
while the browser asks the person at the machine to pick a device,
then `open`; writes made meanwhile are queued and go out when it
connects. Check `stream.transports()` first
to tell a mistyped scheme from one this build does not carry.

**Parameters**

- `url` `string` — Stream URL — scheme://target[?k=v&k=v].
- `opts` `StreamOpenOpts` _(optional)_ — Buffer capacities (optional).

**Returns** `PromiseHandle` — Promise handle for task.await().

```lua
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1&notify=ffe1"))
```

## typed/builtin//modules/api/engine/stream/stream/read {#typed-builtin-modules-api-engine-stream-stream-read}

```lua
stream.read(handle: string, max: number?) -> string
```

Drain up to max buffered inbound bytes from the stream.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `max` `number` _(optional)_ — Maximum bytes to drain (optional). Omit to drain everything buffered.

**Returns** `string` — Drained bytes, byte-safe. "" when none buffered or handle names no open stream.

```lua
local chunk = stream.read(handle)
```

## typed/builtin//modules/api/engine/stream/stream/serialPorts {#typed-builtin-modules-api-engine-stream-stream-serialports}

```lua
stream.serialPorts() -> SerialPorts
```

Every serial device this machine has, for picking the one to open.
`ports` is an array ordered by path. Each entry carries the `path` the
device is at (`/dev/ttyACM0` on Linux, `COM3` on Windows), the `url`
that opens it, the `kind` of bus it attaches by, and — for a USB
device — the `vendorId`, `productId`, `serialNumber`, `manufacturer`
and `product` it advertises.

A device's path moves with enumeration order: a board that came up at
`/dev/ttyACM0` is at `/dev/ttyACM1` once something else is plugged in
first, and moves across `COM3`-`COM5` on Windows. What the device
advertises holds still across those moves, so match on
`vendorId`/`productId` — or on `serialNumber` to tell two of the same
board apart — and open the `url` that entry carries, appending the
port settings `stream.open` documents.

Three answers are distinct. `supported` false with a `reason` means
this platform has no serial bus to enumerate at all. `error` set means
it has one and the operating system refused this enumeration, so a
later call may answer. An empty `ports` with neither means the machine
has no serial device attached, which is an ordinary result.

**Returns** `SerialPorts` — { supported, reason, error, ports } — the platform's answer, this enumeration's, and the devices it found.

```lua
for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end
```

## typed/builtin//modules/api/engine/stream/stream/status {#typed-builtin-modules-api-engine-stream-stream-status}

```lua
stream.status(handle: string) -> StreamStatus?
```

Report what the stream has carried and lost. `state` is where
the stream is in its life: `opening`, `permissionPending` while the
platform asks the person at the machine to allow the connection,
`open`, `denied` when that permission was refused, and `closed`
when it is finished. `pending` is bytes
accepted and not yet handed to the peer; `capacity` is the value
`pending` may reach before a write is refused. `error` holds the
most recent transport failure and the refusal a `denied` stream
carries, retained for the life of the
stream. nil when handle names no open stream.

**Parameters**

- `handle` `string`

**Returns** `StreamStatus?` — Stream status, or nil when handle names no open stream.

```lua
local s = stream.status(handle); print(s.pending, s.capacity)
```

## typed/builtin//modules/api/engine/stream/stream/streams {#typed-builtin-modules-api-engine-stream-stream-streams}

```lua
stream.streams() -> { OpenStream }
```

Every open stream, dialled or answered, in the order they were
opened. Each entry is what `stream.status` reports about it, plus the
`handle` it is addressed by and the `owner` chunk its life follows —
a connection `stream.accept` handed over carries the owner of the
listener that answered it.

**Returns** `{ OpenStream }` — An array of open streams.

```lua
for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end
```

## typed/builtin//modules/api/engine/stream/stream/transports {#typed-builtin-modules-api-engine-stream-stream-transports}

```lua
stream.transports() -> { [string]: TransportSupport }
```

Every stream scheme this build knows about — a capability
probe, in both directions. `supported` answers `stream.open` and
`listen` answers `stream.listen`, since a scheme can carry one and
not the other. Each reason is nil when its direction works,
otherwise it names why not: an unbuilt transport names its own
absence, a transport this platform lacks (`tcp` and `tty` on wasm;
`ble` in a browser without Web Bluetooth or with the radio off,
which the page itself answers) names that, and a loopback stream,
whose peer is itself, names that nothing dials it. A typo'd scheme
is absent from this table entirely, which is what tells it apart
from a real transport this build lacks.

**Returns** `{ [string]: TransportSupport }` — Map of scheme name to { supported, reason, listen, listenReason }.

```lua
local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end
```

## typed/builtin//modules/api/engine/stream/stream/write {#typed-builtin-modules-api-engine-stream-stream-write}

```lua
stream.write(handle: string, bytes: string) -> WriteOutcome
```

Queue bytes for the stream's peer. Never blocks. `"accepted"`
means the bytes were queued. `"full"` means the outbound queue has
no room right now — backpressure, not failure: the peer is alive
and draining slower than this call is producing, so a retry after
it catches up can succeed. Compare `pending` against `capacity` on
`stream.status()` to see it coming before a write is refused.
`"closed"` means the stream is finished, or handle names no open
stream — reopen to continue, retrying never succeeds. `"tooLarge"`
means bytes is bigger than the stream's whole outbound capacity, so
it can never fit at any queue depth — retrying the same write
returns this again.

**Parameters**

- `handle` `string` — Stream handle from stream.open.
- `bytes` `string` — Bytes to queue, byte-safe.

**Returns** `WriteOutcome` — "accepted" | "full" | "closed" | "tooLarge"

```lua
local outcome = stream.write(handle, data)
```
