Lori Package¶
Lori is a TCP networking library that separates connection logic from actor
scheduling. Unlike the standard library's net package, which bakes connection
handling into a single actor, lori puts the TCP state machine in a plain
TCPConnection class that your actor delegates to.
This separation gives you control over how your actor is structured while lori
handles the low-level I/O.
To build a TCP application with lori, you implement an actor that mixes in two
traits: TCPConnectionActor (which wires up
the ASIO event plumbing) and a lifecycle event receiver
(ServerLifecycleEventReceiver or
ClientLifecycleEventReceiver) that
delivers callbacks like _on_received, _on_connected, and _on_closed.
Echo Server¶
Here is a complete echo server. It has two actors: a listener that accepts connections and a connection handler that echoes data back to the client.
use "lori"
actor Main
new create(env: Env) =>
EchoServer(TCPListenAuth(env.root), "", "7669", env.out)
actor EchoServer is TCPListenerActor
var _tcp_listener: TCPListener = TCPListener.none()
let _out: OutStream
let _server_auth: TCPServerAuth
new create(listen_auth: TCPListenAuth,
host: String,
port: String,
out: OutStream)
=>
_out = out
_server_auth = TCPServerAuth(listen_auth)
_tcp_listener = TCPListener(listen_auth, host, port, this)
fun ref _listener(): TCPListener =>
_tcp_listener
fun ref _on_accept(fd: U32): Echoer =>
Echoer(_server_auth, fd, _out)
fun ref _on_listening() =>
_out.print("Echo server started.")
fun ref _on_listen_failure() =>
_out.print("Couldn't start Echo server.")
actor Echoer is (TCPConnectionActor & ServerLifecycleEventReceiver)
var _tcp_connection: TCPConnection = TCPConnection.none()
let _out: OutStream
new create(auth: TCPServerAuth, fd: U32, out: OutStream) =>
_out = out
_tcp_connection = TCPConnection.server(auth, fd, this, this)
fun ref _connection(): TCPConnection =>
_tcp_connection
fun ref _on_received(data: Array[U8] iso) =>
_tcp_connection.send(consume data)
fun ref _on_closed() =>
_out.print("Connection closed.")
The listener actor implements
TCPListenerActor. It owns a
TCPListener and must provide _listener() to
return it. When a client connects, _on_accept is called with the raw file
descriptor. You create and return a connection-handling actor from there.
The connection handler implements both TCPConnectionActor and
ServerLifecycleEventReceiver. It owns a TCPConnection and must provide
_connection() to return it. Data arrives via _on_received.
Note the TCPConnection.none() and TCPListener.none() field initializers.
Pony requires fields to be initialized before the constructor body runs, but the
real connection setup happens asynchronously. The none() constructors provide
safe placeholder values that are replaced by real initialization via the
_finish_initialization behavior.
Client¶
Here is a client that connects to a server and sends a message:
use "lori"
actor MyClient is (TCPConnectionActor & ClientLifecycleEventReceiver)
var _tcp_connection: TCPConnection = TCPConnection.none()
new create(auth: TCPConnectAuth, host: String, port: String) =>
_tcp_connection = TCPConnection.client(auth, host, port, "", this, this)
fun ref _connection(): TCPConnection =>
_tcp_connection
fun ref _on_connected() =>
_tcp_connection.send("Hello, server!")
fun ref _on_connection_failure() =>
// All connection attempts failed
None
fun ref _on_received(data: Array[U8] iso) =>
// Handle response from server
None
Clients use ClientLifecycleEventReceiver instead of
ServerLifecycleEventReceiver. The key difference is the connection lifecycle:
clients get _on_connecting (called as connection attempts are in progress),
_on_connected (ready for data), and _on_connection_failure (all attempts
failed). Servers get _on_started (ready for data) and _on_start_failure.
Sending Data¶
Unlike many networking libraries, send() is fallible. It returns
(SendToken | SendError) rather than silently dropping data:
match _tcp_connection.send("some data")
| let token: SendToken =>
// Data accepted. token will arrive in _on_sent when fully written.
None
| SendErrorNotConnected =>
// Connection is not open.
None
| SendErrorNotWriteable =>
// Under backpressure. Wait for _on_unthrottled before retrying.
None
end
SendToken is an opaque value identifying the send
operation. When the data has been fully handed to the OS, lori delivers the
same token to _on_sent. If the connection closes while a write is still
partially pending, _on_send_failed fires instead. Both callbacks always arrive
in a subsequent behavior turn, never during send() itself.
The library does not queue data during backpressure. When send() returns
SendErrorNotWriteable, the application
decides what to do: queue, drop, or close. Use _on_throttled and
_on_unthrottled to track backpressure state, or check is_writeable() before
calling send().
SSL¶
Adding SSL to a connection requires only a constructor change. Use
TCPConnection.ssl_client or TCPConnection.ssl_server with an
SSLContext val:
use "lori"
use "ssl/net"
actor SSLEchoer is (TCPConnectionActor & ServerLifecycleEventReceiver)
var _tcp_connection: TCPConnection = TCPConnection.none()
new create(auth: TCPServerAuth, sslctx: SSLContext val, fd: U32) =>
_tcp_connection = TCPConnection.ssl_server(auth, sslctx, fd, this, this)
fun ref _connection(): TCPConnection =>
_tcp_connection
fun ref _on_received(data: Array[U8] iso) =>
_tcp_connection.send(consume data)
fun ref _on_start_failure() =>
// SSL handshake failed
None
SSL is handled entirely inside TCPConnection. The handshake runs
transparently after the TCP connection is established, and _on_connected
(client) or _on_started (server) fires only after the handshake completes. If
the handshake fails, clients get _on_connection_failure and servers get
_on_start_failure. The rest of the application code (sending, receiving,
closing) is identical to the non-SSL case.
TLS Upgrade (STARTTLS)¶
Some protocols (PostgreSQL, SMTP, LDAP) require upgrading an existing plaintext
connection to TLS mid-stream. Use start_tls() on an established connection to
initiate a TLS handshake:
use "lori"
use "ssl/net"
actor MyStartTLSClient is (TCPConnectionActor & ClientLifecycleEventReceiver)
var _tcp_connection: TCPConnection = TCPConnection.none()
let _sslctx: SSLContext val
new create(auth: TCPConnectAuth, sslctx: SSLContext val,
host: String, port: String)
=>
_sslctx = sslctx
_tcp_connection = TCPConnection.client(auth, host, port, "", this, this)
fun ref _connection(): TCPConnection =>
_tcp_connection
fun ref _on_connected() =>
// Send protocol-specific upgrade request over plaintext
_tcp_connection.send("STARTTLS")
fun ref _on_received(data: Array[U8] iso) =>
let msg = String.from_array(consume data)
if msg == "OK" then
// Server agreed to upgrade — initiate TLS handshake
match _tcp_connection.start_tls(_sslctx, "localhost")
| let err: StartTLSError => None // handle error
end
end
fun ref _on_tls_ready() =>
// TLS handshake complete — now sending encrypted data
_tcp_connection.send("encrypted payload")
fun ref _on_tls_failure() =>
// TLS handshake failed — _on_closed will follow
None
start_tls() returns None when the handshake has been started, or a
StartTLSError if the upgrade cannot proceed. The
connection must be open, not already TLS, not muted, and have no buffered read
data or pending writes. During the handshake, send() returns
SendErrorNotConnected. When the handshake completes, _on_tls_ready() fires.
If it fails, _on_tls_failure() fires followed by _on_closed().
Connection Limits¶
TCPListener accepts an optional limit parameter to cap the number of
concurrent connections:
// Accept at most 100 connections at a time
_tcp_listener = TCPListener(listen_auth, host, port, this, 100)
When the limit is reached, the listener pauses accepting. As connections close, it resumes automatically. The default is no limit.
Auth Hierarchy¶
Lori uses Pony's object capability model for authorization. Each operation requires a specific auth token, and tokens form a hierarchy — a more powerful token can create a less powerful one:
NetAuth(fromAmbientAuth) — general network accessTCPAuth(fromAmbientAuthorNetAuth) — any TCP operationTCPListenAuth(fromAmbientAuth,NetAuth, orTCPAuth) — open a listenerTCPConnectAuth(fromAmbientAuth,NetAuth, orTCPAuth) — open a client connectionTCPServerAuth(fromAmbientAuth,NetAuth,TCPAuth, orTCPListenAuth) — handle an accepted server connection
In practice, Main creates the auth tokens it needs from env.root and passes
them to the actors that need them. The echo server example above shows the
typical pattern: Main creates a TCPListenAuth, the listener creates a
TCPServerAuth from it, and each accepted connection receives that
TCPServerAuth.
Public Types¶
- trait ClientLifecycleEventReceiver
- type EitherLifecycleEventReceiver
- type MaxSpawn
- primitive NetAuth
- primitive OSSockOpt
- primitive PonyAsio
- primitive PonyTCP
- type SendError
- primitive SendErrorNotConnected
- primitive SendErrorNotWriteable
- class SendToken
- trait ServerLifecycleEventReceiver
- primitive StartTLSAlreadyTLS
- type StartTLSError
- primitive StartTLSNotConnected
- primitive StartTLSNotReady
- primitive StartTLSSessionFailed
- primitive TCPAuth
- primitive TCPConnectAuth
- class TCPConnection
- trait TCPConnectionActor
- primitive TCPListenAuth
- class TCPListener
- trait TCPListenerActor
- primitive TCPServerAuth