Transport-layer buffering and message I/O.
- recvBuf/sendBuf management (compact, fill, send)
- Synchronous backend-message parsing (nextMessage) and the async wrapper recvMessage
- Notification/Notice dispatch (called from nextMessage)
- Transport teardown (closeTransport)
- TCP keepalive / TCP_NODELAY socket options
- Host helpers (isUnixSocket, unixSocketPath, getHosts)
- makeCopyOutCallback / makeCopyInCallback cross-backend templates
The host helpers and makeCopy* templates are re-exported through pg_connection.nim; the transport buffering machinery stays here for sibling modules and tests. Depends only on types.nim and the protocol/error/backend abstraction modules.
Internal module: not part of the public API. Import the pg_connection hub instead; what it re-exports is the supported surface (see tests/api_surface.golden).
Types
RecvWatch = ref object
-
Background read watch for unsolicited messages during send (at most one).
Settle it (take + await, or cancel) before reusing the normal recv path: an unsettled read shares recvBuf with whatever runs next.
Procs
proc closeTransport(conn: PgConnection): owned(Future[void]) {. ...stackTrace: false, raises: [Exception, LibraryError, SslError, ValueError], tags: [RootEffect], forbids: [].}
-
Close transport resources without sending Terminate.
Re-entrant: a racing second teardown awaits the first rather than returning early on the already-detached handles, so a resolved close() still means the fd and the backend session are released.
proc compactRecvBuf(conn: PgConnection) {.inline, ...raises: [], tags: [], forbids: [].}
- Compact recvBuf (caller checks csClosed). Only safe before reading new data from the socket: it moves bytes an in-flight read still points at.
proc configureKeepalive(fd: posix.SocketHandle; config: ConnConfig) {. ...raises: [PgConnectionError], tags: [], forbids: [].}
- Set TCP keepalive options on the socket.
proc configureTcpNoDelay(fd: posix.SocketHandle) {....raises: [], tags: [], forbids: [].}
- Disable Nagle's algorithm for low-latency sends.
proc dispatchNotice(conn: PgConnection; msg: BackendMessage) {....raises: [], tags: [RootEffect], forbids: [].}
proc dispatchNotification(conn: PgConnection; msg: BackendMessage) {....raises: [], tags: [RootEffect], forbids: [].}
proc enqueueNotification(conn: PgConnection; notif: Notification) {....raises: [], tags: [RootEffect], forbids: [].}
- Enqueue under notifyMaxQueue (<=0 = unbounded); drop oldest on overflow.
proc fillRecvBuf(conn: PgConnection; timeout: Duration = ZeroDuration): Future[ void] {....stackTrace: false, raises: [Exception, PgStateError, PgConnectionError, AsyncTimeoutError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect], forbids: [].}
- Read into recvBuf. AsyncTimeoutError: caller handles state; other errors → csClosed + raiseTransportFailure.
proc getHosts(config: ConnConfig): seq[HostEntry] {....raises: [], tags: [], forbids: [].}
- Return the list of hosts to try. If hosts is populated, return it; otherwise synthesize a single entry from host/port.
proc isConnected(conn: PgConnection): bool {....raises: [], tags: [], forbids: [].}
- Transport present and no kernel FIN/RST observed (cheap, non-blocking; use ping for full check).
proc isUnixSocket(host: string): bool {.inline, ...raises: [], tags: [], forbids: [].}
- True if host represents a Unix socket directory (starts with '/'). Compatible with libpq behavior.
proc nextMessage(conn: PgConnection; rowData: RowData = nil; rowCount: ptr int32 = nil; onRow: RowCallback = nil; onRowError: ptr ref CatchableError = nil; skipDataRow: bool = false): Option[BackendMessage] {. ...raises: [PgProtocolError], tags: [RootEffect], forbids: [].}
-
Parse next message from recvBuf (none = incomplete). Dispatches notify/notice, consumes ParameterStatus/DataRow (streaming via onRow); skipDataRow avoids decode. Error → csClosed.
onRow requires onRowError: callback failures are deferred into that slot so the pump can drain to ReadyForQuery. Without a slot the first DataRow would dereference nil, so the missing slot is rejected up front with PgProtocolError (the connection is left open — unlike wire corruption, this is a caller bug, not a broken peer).
proc rearm(w: RecvWatch; conn: PgConnection) {....raises: [Exception, PgStateError, PgConnectionError, AsyncTimeoutError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect], forbids: [].}
- Resume watching with a fresh background read. Only call once the previous read has been consumed (take + await), never while one is still in flight.
proc reclaimHandoff(conn: PgConnection) {....raises: [], tags: [], forbids: [].}
- Requeue a handoff whose waiter will never claim it, so an abandoned frame cannot make the notification unreachable.
proc recvMessage(conn: PgConnection; timeout = ZeroDuration; rowData: RowData = nil; rowCount: ptr int32 = nil): Future[ BackendMessage] {....stackTrace: false, raises: [Exception, ValueError, PgProtocolError, PgStateError, PgConnectionError, CancelledError, CatchableError, AsyncTimeoutError], tags: [RootEffect, TimeEffect], forbids: [].}
- Receive one message (nextMessage + fillRecvBuf); timeout → csClosed.
proc requeueHandoff(conn: PgConnection; notif: Notification) {....raises: [], tags: [], forbids: [].}
- Requeue an unconsumed handoff at the front.
proc resetWireState(conn: PgConnection) {....raises: [], tags: [], forbids: [].}
-
Forget what the wire's previous life left behind: the buffered bytes on both sides and the replies the old backend owed.
Sole owner of that reset: a stale count carried onto a fresh backend would dial a CancelRequest at an unrelated PID and retire a healthy connection.
proc sendBufMsg(conn: PgConnection): Future[void] {....stackTrace: false, raises: [ Exception, CancelledError, PgStateError, CatchableError, PgConnectionError], tags: [RootEffect], forbids: [].}
- Send sendBuf (copied; safe to mutate after call); failure → csClosed. Books what the buffer leaves owed; see sendMsg.
proc sendMsg(conn: PgConnection; data: seq[byte]): Future[void] {. ...stackTrace: false, raises: [Exception, CancelledError, PgStateError, CatchableError, PgConnectionError], tags: [RootEffect], forbids: [].}
- Send raw bytes; failure → csClosed. Books what they leave owed first; see noteWrite.
proc sendRawBytes(socket: AsyncSocket; data: seq[byte]): Future[void] {. ...raises: [Exception, SslError, ValueError], tags: [RootEffect], forbids: [].}
- Send seq[byte] via asyncdispatch socket.
proc sendRawData(socket: AsyncSocket; p: pointer; len: int): Future[void] {. ...raises: [Exception, SslError, ValueError], tags: [RootEffect], forbids: [].}
- Send raw bytes via asyncdispatch socket. Copies data into a string once.
proc socketHasFin(conn: PgConnection): bool {....raises: [], tags: [], forbids: [].}
- POSIX half-open probe (MSG_PEEK): true if FIN/RST observed; false otherwise or unavailable.
proc socketHasPendingData(conn: PgConnection): bool {....raises: [], tags: [], forbids: [].}
- True if kernel has readable bytes (pre-TLS injection check; kernel buffer only).
proc startRecvWatch(conn: PgConnection): RecvWatch {....raises: [Exception, PgStateError, PgConnectionError, AsyncTimeoutError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect], forbids: [].}
- Begin watching for an unsolicited backend message. The bytes are committed to recvBuf when the read completes; poll with ready, then take + await (immediate once ready) and parse with nextMessage.
proc unixSocketPath(host: string; port: int): string {....raises: [], tags: [], forbids: [].}
- Build the libpq-compatible Unix socket file path: {dir}/.s.PGSQL.{port}.
Templates
template makeCopyInCallback(body: untyped): CopyInCallback
-
Create a CopyInCallback that works with both asyncdispatch and chronos. body must evaluate to seq[byte]. Return an empty seq to signal completion.
var idx = 0 let rows = @["1\tAlice\n".toBytes(), "2\tBob\n".toBytes()] let cb = makeCopyInCallback: if idx < rows.len: let chunk = rows[idx] inc idx chunk else: newSeq[byte]()
template makeCopyOutCallback(body: untyped): CopyOutCallback
-
Create a CopyOutCallback that works with both asyncdispatch and chronos. Inside body, the current chunk is available as data: sink seq[byte].
var chunks: seq[seq[byte]] let cb = makeCopyOutCallback: chunks.add(data)
template pumpUntilReady(conn: PgConnection; body: untyped; readyBody: untyped) {. dirty.}
- Bare pump (skipDataRow=true; for callers that discard rows).
template pumpUntilReady(conn: PgConnection; resultData: untyped; onRow: untyped; onRowErr: untyped; body: untyped; readyBody: untyped) {. dirty.}
- Streaming pump (onRow per row; first error in onRowErr).
template pumpUntilReady(conn: PgConnection; resultData: untyped; rowCountPtr: untyped; body: untyped; readyBody: untyped) {. dirty.}
- Pump until ReadyForQuery; pumpMsg/queryError injected into body/readyBody.