Log inGet started

stream

The stream namespace — the engine's Luau API reference for stream.

The stream namespace — 12 functions.

globals/stream/accept

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.

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

globals/stream/close

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.

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

globals/stream/listen

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

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 string — Promise handle for task.await().

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

globals/stream/listenerStatus

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.

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

globals/stream/listeners

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.

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

globals/stream/open

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

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 string — Promise handle for task.await().

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

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.

local chunk = stream.read(handle)

globals/stream/serialPorts

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.

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

globals/stream/status

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.

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

globals/stream/streams

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.

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

globals/stream/transports

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 }.

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

globals/stream/write

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"

local outcome = stream.write(handle, data)
  • api
  • reference