# This file is auto-generated by stubs/generate_stubs.py – do not edit manually.
# Re-run the script after changing any .pxi / .pyx source.
from __future__ import annotations

import concurrent.futures
import sys
from collections.abc import AsyncGenerator, Callable
from typing import Any, ClassVar, Self

if sys.version_info >= (3, 12):
    from collections.abc import Buffer
else:
    from typing import TypeAlias
    Buffer: TypeAlias = memoryview

# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------

class NngError(Exception):
    """
    Base exception for all nng errors.

    ``code`` is the raw nng error integer.
    """
    code: int
    def __init__(self, code: int, message: str | None = None) -> None: ...
    def __str__(self) -> str: ...

class NngTimeout(NngError, TimeoutError):
    """NNG_ETIMEDOUT – operation timed out."""
    ...

class NngConnectionRefused(NngError, ConnectionRefusedError):
    """NNG_ECONNREFUSED – connection refused."""
    ...

class NngClosed(NngError):
    """NNG_ECLOSED – socket / pipe / aio is closed."""
    ...

class NngAgain(NngError):
    """NNG_EAGAIN – operation would block in non-blocking mode."""
    ...

class NngNotSupported(NngError, NotImplementedError):
    """NNG_ENOTSUP – operation not supported by this protocol."""
    ...

class NngAddressInUse(NngError, OSError):
    """NNG_EADDRINUSE – address already in use."""
    ...

class NngPermission(NngError, PermissionError):
    """NNG_EPERM – permission denied."""
    ...

class NngCanceled(NngError):
    """NNG_ECANCELED – operation was cancelled."""
    ...

class NngConnectionReset(NngError, ConnectionResetError):
    """NNG_ECONNRESET – connection reset by peer."""
    ...

class NngConnectionAborted(NngError, ConnectionAbortedError):
    """NNG_ECONNABORTED – connection aborted."""
    ...

class NngNoMemory(NngError, MemoryError):
    """NNG_ENOMEM – out of memory."""
    ...

class NngInvalidArgument(NngError, ValueError):
    """NNG_EINVAL – invalid argument."""
    ...

class NngState(NngError):
    """NNG_ESTATE – incorrect state for this operation."""
    ...

class NngAuthError(NngError):
    """NNG_EPEERAUTH – peer authentication failed."""
    ...

class NngCryptoError(NngError):
    """NNG_ECRYPTO – cryptographic error."""
    ...
# ---------------------------------------------------------------------------
# Pipe lifecycle enum
# ---------------------------------------------------------------------------

class PipeStatus(int):
    """
    Lifecycle state of a :class:`Pipe`.

    * ``ADDING``  – the pipe has been created and negotiated (ADD_PRE event).
      The :meth:`Socket.on_new_pipe` callback fires at this point.
    * ``ACTIVE``  – the pipe is fully active (ADD_POST event).
      The pipe's own ``on_status_change`` callback fires here.
    * ``REMOVED`` – the pipe has been closed (REM_POST event).
      The pipe's ``on_status_change`` callback fires, then the pipe is
      removed from :attr:`Socket.pipes`.
    """
    ADDING: ClassVar[PipeStatus]
    ACTIVE: ClassVar[PipeStatus]
    REMOVED: ClassVar[PipeStatus]

# ---------------------------------------------------------------------------
# Core types
# ---------------------------------------------------------------------------

class Message:
    """
    A bytes buffer managed by nng.

    Messages are the unit of data transfer in nng.  They consist of a header
    and a body, which can be edited.  The body supports the buffer protocol
    for zero-copy access to the underlying nng buffer.

    When passing any data to send methods, a Message is internally
    constructed to hold a copy of that data.

    Using Message directly to build your message enables to save on
    a data copy which can make a difference for large message.

    Another usage of Message is to forward message from a socket
    to another. `recv` methods return a Message instance which
    can be directly passed to again to `send` methods. This avoids
    a buffer copy. In this use case, it can be useful to
    append or modify the message before forwarding it. For that use,
    Message provides methods to resize/trim/append data.

    When a Message is sent to a send method (except when send fails),
    the ownership of its content is passed to NNG, and the Message
    cannot be used anymore. `dup()` can be used to create an independent
    copy of the Message if you want to reuse the same content for multiple sends.

    For the reason above, when writing into a Message using a wrapper object
    (for instance memoryview or numpy asarray), you must release the wrapper
    objects before sending the Message, otherwise the send will fail with a
    PermissionError.

    Examples:

    Build a message from bytes and send it through a socket::

        msg = Message(b"hello") # create a message and copies to it "hello"
        sock.send(msg)          # ownership of msg's content is transferred to nng

    Fill as 2D array and send it::

        msg = Message(size=10*10*4)  # pre-allocate a message body of 400 bytes
        arr = np.asarray(msg, shape=(10, 10), dtype=np.float32)
        arr[:] = np.random.rand(10, 10) # fill the message body with random data
        del arr                         # release the numpy array wrapper
        sock.send(msg)  # ownership of msg's content is transferred to nng

    Receive a message and forward it after appending an ID::

        msg = sock1.recv()     # receive a message from sock1
        msg.append_u64(12345)  # append an ID to the message body
        sock2.send(msg)        # forward the same message to sock2 (zero-copy)
    """
    def __init__(self, data: bytes | Buffer | bytearray | str | None = None, *, size: int = 0) -> None:
        """
        Allocate a new message.

        Parameters
        ----------
        data:
            Initial body (bytes / bytearray / buffer). *size* is ignored
            when *data* is provided.
            str is accepted, in which case it is encoded as UTF-8.
        size:
            Pre-allocate a body of *size* zero bytes.
        """
        ...

    @property
    def body(self) -> memoryview:
        """Zero-copy writeable view of the message body."""
        ...

    @property
    def header(self) -> bytes:
        """
        Copy of the message header as *bytes*.

        The header is a separate byte buffer reserved
        for internal use by nng.
        """
        ...

    @property
    def pipe(self) -> Pipe | None:
        """
        The :class:`Pipe` this message arrived on, or ``None``.

        Returns ``None`` if:
            - The message was not received over a socket pipe (fresh allocation)
            - The message was received over a pipe that has since been closed.

        Else returns the pipe.
        """
        ...

    @property
    def pipe_id(self) -> int:
        """ID of the pipe this message arrived on (0 if not set)."""
        ...

    def append(self, data: bytes | Message | Buffer | bytearray | str) -> None:
        """Append *data* to the end of the body."""
        ...

    def append_u16(self, v: int) -> None:
        """Append a 16-bit unsigned integer in network byte order to the end of the body."""
        ...

    def append_u32(self, v: int) -> None:
        """Append a 32-bit unsigned integer in network byte order to the end of the body."""
        ...

    def append_u64(self, v: int) -> None:
        """Append a 64-bit unsigned integer in network byte order to the end of the body."""
        ...

    def chop(self, n: int) -> None:
        """Remove *n* bytes from the end of the body."""
        ...

    def chop_u16(self) -> int:
        """Remove and return a 16-bit unsigned integer in network byte order from the end of the body."""
        ...

    def chop_u32(self) -> int:
        """Remove and return a 32-bit unsigned integer in network byte order from the end of the body."""
        ...

    def chop_u64(self) -> int:
        """Remove and return a 64-bit unsigned integer in network byte order from the end of the body."""
        ...

    def clear(self) -> None:
        """
        Discard all body bytes (append).

        Resets the body length to zero without releasing the pre-allocated
        capacity. Use this to reuse a message buffer with append calls
        without paying for a new allocation.
        """
        ...

    def dup(self) -> Message:
        """Return an independent copy of this message."""
        ...

    def insert(self, data: bytes | Message | Buffer | bytearray | str) -> None:
        """Prepend *data* to the front of the body."""
        ...

    def reserve(self, capacity: int) -> None:
        """
        Ensure the body buffer has at least *capacity* bytes pre-allocated.

        Pre-reserving the total anticipated size avoids many small
        reallocations when building a message incrementally with
        ``append`` / ``insert``.
        """
        ...

    def resize(self, new_size: int) -> None:
        """Resize the body to *new_size* bytes, truncating or zero-padding as needed."""
        ...

    def to_bytes(self) -> bytes:
        """Copy the message body into a new *bytes* object."""
        ...

    def trim(self, n: int) -> None:
        """Remove *n* bytes from the front of the body."""
        ...

    def trim_u16(self) -> int:
        """Remove and return a 16-bit unsigned integer in network byte order from the front of the body."""
        ...

    def trim_u32(self) -> int:
        """Remove and return a 32-bit unsigned integer in network byte order from the front of the body."""
        ...

    def trim_u64(self) -> int:
        """Remove and return a 64-bit unsigned integer in network byte order from the front of the body."""
        ...

    def __buffer__(self, flags: Any) -> Any:
        """Return a buffer object that exposes the underlying memory of the object."""
        ...

    def __eq__(self, *args: Any, **kwargs: Any) -> bool:
        """True when both messages have identical body bytes (memcmp)."""
        ...

    def __ge__(self, value: Any) -> Any:
        """Return self>=value."""
        ...

    def __gt__(self, value: Any) -> Any:
        """Return self>value."""
        ...

    def __le__(self, value: Any) -> Any:
        """Return self<=value."""
        ...

    def __len__(self) -> int:
        """Return len(self)."""
        ...

    def __lt__(self, value: Any) -> Any:
        """Return self<value."""
        ...

    def __ne__(self, value: Any) -> bool:
        """Return self!=value."""
        ...

    def __release_buffer__(self, buffer: Any) -> Any:
        """Release the buffer object that exposes the underlying memory of the object."""
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...

    def __str__(self) -> str:
        """Return str(self)."""
        ...


class TlsConfig:
    """
    TLS configuration object for securing nng connections.

    Pass as the ``tls=`` argument to :meth:`Socket.add_dialer` or
    :meth:`Socket.add_listener`.

    A single :class:`TlsConfig` may be shared across multiple dialers and
    listeners.  However, once a configuration is attached to a service for
    the first time, it becomes **read-only**: any further attempt to modify
    it (call any method on it) will raise an error.

    Example (mTLS client)::

        cfg = TlsConfig(server=False)    # client mode (default)
        cfg.server_name("example.com")   # SNI + cert hostname check
        cfg.ca_chain(ca_pem)             # CA to validate the server
        cfg.own_cert(cert_pem, key_pem)  # client certificate for mTLS
        sock.add_dialer("tls+tcp://example.com:5555", tls=cfg).start()

    Example (TLS server)::

        cfg = TlsConfig(server=True)
        cfg.cert_key_file("/etc/ssl/server.pem")
        sock.add_listener("tls+tcp://0.0.0.0:5555", tls=cfg).start()
    """
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        ...

    def auth_mode(self, mode: int) -> TlsConfig:
        """
        Set the peer certificate verification mode.

        *mode* must be one of the module-level constants:

        * ``TLS_AUTH_NONE`` — no peer authentication.  Default for **server**
          configurations, where clients typically don't present certificates.
        * ``TLS_AUTH_OPTIONAL`` — verify the peer's certificate if one is
          presented; allow the session to proceed even if none is given.
        * ``TLS_AUTH_REQUIRED`` — the peer **must** present a valid certificate;
          connections without one are rejected.  Default for **client**
          configurations.
        """
        ...

    def ca_chain(self, cert_pem: str, crl_pem: str | None = None) -> TlsConfig:
        """
        Add a CA certificate chain (and optional CRL) used to validate peers.

        *cert_pem* is a PEM string containing one or more X.509 CA
        certificates; multiple certificates may be concatenated, with the
        leaf listed first.

        *crl_pem* is an optional PEM string containing a certificate
        revocation list (CRL) for the associated authority.  Pass ``None``
        to omit.

        This method may be called **multiple times** to add additional chains
        without affecting those already added.

        .. note::
            CA certificates **must** be configured when the authentication
            mode is ``TLS_AUTH_REQUIRED`` (the default for clients).
        """
        ...

    def ca_file(self, path: str) -> TlsConfig:
        """
        Load CA certificates (and optional CRLs) from a PEM file.

        The file at *path* must contain at least one X.509 certificate in PEM
        format and may contain multiple certificates as well as zero or more
        PEM CRL objects.  This information is used to validate peer
        certificates.

        This method may be called **multiple times** to add additional chains
        without affecting those already loaded.

        .. note::
            CA certificates **must** be configured when the authentication
            mode is ``TLS_AUTH_REQUIRED`` (the default for clients).
        """
        ...

    def cert_key_file(self, path: str, password: str | None = None) -> TlsConfig:
        """
        Load our own certificate and private key from a PEM file.

        The file at *path* must contain both the certificate (or chain) and
        the matching private key in PEM format.  When a chain is present,
        the leaf certificate should appear first; the self-signed root may
        be omitted.

        If the private key is encrypted, supply the decryption *password*;
        pass ``None`` otherwise.

        On **server** configurations this method may be called **multiple
        times** to register certificates for different cryptographic
        algorithms (e.g. RSA and ECDSA).  On **client** configurations it
        should only be called once.
        """
        ...

    def own_cert(self, cert_pem: str, key_pem: str, password: str | None = None) -> TlsConfig:
        """
        Configure our own certificate and private key (PEM strings).

        *cert_pem* is the local certificate (or chain) in PEM format.  When
        providing a chain, list the leaf certificate first; the self-signed
        root may be omitted because the peer must already have it.

        *key_pem* is the matching private key in PEM format.  If the key is
        encrypted, supply the decryption *password*; pass ``None`` otherwise.

        .. warning::
            This method must only be called **once** per configuration object.
            Calling it a second time raises an error.  Use
            :meth:`cert_key_file` instead, which supports multiple calls on
            server configurations.
        """
        ...

    def psk(self, identity: Any, key: Any) -> Any:
        """
        Configure a pre-shared key (PSK) for TLS connections.

        PSK is an alternative to certificate-based authentication.

        *identity* is a printable string that acts as a public identifier
        (similar to a user name).  Embedded NUL bytes are not supported.
        Typical implementations support identities up to 64 bytes.

        *key* is the secret bytes used to derive session keys.  Typical
        implementations support keys up to 32 bytes.

        In **client** mode this method should be called **once** to set the
        client's own identity and key.

        In **server** mode this method may be called **multiple times** to
        register keys for different clients; the server will look up the
        appropriate key when a client connects using its identity.

        .. note::
            PSK support depends on the underlying TLS engine.  Not all
            engines expose this functionality.
        """
        ...

    def server_name(self, name: str) -> TlsConfig:
        """
        Set the expected remote server name (client mode only).

        Used in two ways:

        * **Certificate verification**: the supplied *name* is matched against
          the identity presented in the server's certificate.
        * **SNI hint**: when Server Name Indication is used, *name* is sent
          to the server so it can select the appropriate certificate.

        .. note::
            This method is only meaningful when the configuration is used in
            client mode.
        """
        ...

    def version(self, min_ver: int, max_ver: int) -> TlsConfig:
        """
        Restrict the allowed TLS protocol versions.

        *min_ver* and *max_ver* must be the module-level constants
        ``TLS_VERSION_1_2`` or ``TLS_VERSION_1_3``.  Both endpoints negotiate
        the highest mutually supported version in the allowed range.

        When this method is **not called**, nng defaults to accepting both
        TLS 1.2 and TLS 1.3 (subject to what the underlying TLS engine
        supports).

        .. note::
            Only TLS 1.2 and 1.3 are supported.  SSL and earlier TLS versions
            are insecure and rejected by nng.  Some engines may not support
            restricting the *maximum* version.  0-RTT resumption is not
            supported.
        """
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class Pipe:
    """
    Non-owning wrapper for a single nng pipe (physical connection).

    Pipes are created automatically:

    * A :class:`Dialer` creates one pipe when its outbound connection succeeds
      and recreates it transparently after each reconnect.
    * A :class:`Listener` creates one pipe for every inbound connection it
      accepts.

    Applications normally never create ``Pipe`` objects directly.  They appear
    as arguments to callbacks registered with :meth:`Socket.on_new_pipe`.
    The ``Pipe`` object is **non-owning** — closing it kicks the connection;
    the underlying socket is unaffected.
    """
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        ...

    @property
    def dialer(self) -> Dialer | None:
        """The :class:`Dialer` that created this pipe, or ``None``."""
        ...

    @property
    def id(self) -> int:
        """Numeric pipe ID."""
        ...

    @property
    def listener(self) -> Listener | None:
        """The :class:`Listener` that accepted this pipe, or ``None``."""
        ...

    @property
    def on_status_change(self) -> Callable[[], None] | None:
        """Callback fired when this pipe transitions to ACTIVE or REMOVED."""
        ...
    @on_status_change.setter
    def on_status_change(self, value: Callable[[], None]) -> None: ...

    @property
    def socket(self) -> Socket | None:
        """The owning :class:`Socket`, or ``None`` if no longer tracked."""
        ...

    @property
    def status(self) -> PipeStatus:
        """Current :class:`PipeStatus` of this pipe."""
        ...

    def close(self) -> None:
        """
        Forcibly close this connection.

        The underlying transport is terminated immediately.  Dialers attempt to
        reconnect; listeners wait for a new inbound connection.  In-flight
        sends/receives on this pipe fail immediately.

        If a change callback is registered it fires when the close completes.
        """
        ...

    def __eq__(self, value: Any) -> bool:
        """Return self==value."""
        ...

    def __ge__(self, value: Any) -> Any:
        """Return self>=value."""
        ...

    def __gt__(self, value: Any) -> Any:
        """Return self>value."""
        ...

    def __hash__(self) -> int:
        """Return hash(self)."""
        ...

    def __index__(self) -> int:
        """Return self converted to an integer, if self is suitable for use as an index into a list."""
        ...

    def __le__(self, value: Any) -> Any:
        """Return self<=value."""
        ...

    def __lt__(self, value: Any) -> Any:
        """Return self<value."""
        ...

    def __ne__(self, value: Any) -> bool:
        """Return self!=value."""
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class Dialer:
    """
    An outbound endpoint that connects a :class:`Socket` to a remote peer.

    Each ``Dialer`` establishes and maintains **exactly one outbound connection**
    (called a *pipe*) to its target address.  If the connection is lost, the
    dialer automatically retries in the background using exponential back-off
    between :attr:`reconnect_min_ms` and :attr:`reconnect_max_ms`.

    A single socket can have **multiple dialers**, each pointing at a different
    address.  All resulting pipes are pooled at the socket level and the
    protocol decides how to distribute messages across them.

    Returned by :meth:`Socket.add_dialer` (not yet started; call :meth:`start`
    to initiate the connection).

    All configuration options (TLS, reconnect intervals, timeouts) are set
    via :meth:`Socket.add_dialer` before the dialer is started.  Once
    :meth:`start` is called, the dialer's configuration is **locked** and
    individual options can no longer be changed.

    .. note::
        The dialer/listener distinction is **orthogonal** to the
        client/server role in the protocol.  For example, a ``REP`` socket
        may use a dialer to connect to a ``REQ`` socket's listener.

    The dialer is closed automatically when its parent socket is closed.
    It is also closed explicitly via :meth:`close` or when used as a
    context manager.

    The properties on this class are **read-only** and exist solely for
    inspection after the fact.

    Use as a context manager to ensure the endpoint is closed::

        with sock.add_dialer("tcp://127.0.0.1:5555",
                             reconnect_min_ms=100, reconnect_max_ms=5000) as d:
            d.start()
            ...
    """
    @property
    def id(self) -> int:
        """The numeric dialer ID, or 0 if the dialer has been closed."""
        ...

    @property
    def pipe(self) -> Pipe | None:
        """The active :class:`Pipe` for this dialer, or ``None`` if not connected."""
        ...

    @property
    def reconnect_max_ms(self) -> Any:
        """
        Maximum reconnect interval in milliseconds.

        The dialer will not wait longer than this between retries.  ``0``
        means use :attr:`reconnect_min_ms` as a fixed interval.  Configured
        at construction time via :meth:`Socket.add_dialer`.

        Raises
        ------
        NngClosed
            If the dialer has been closed.
        """
        ...

    @property
    def reconnect_min_ms(self) -> Any:
        """
        Minimum reconnect interval in milliseconds.

        After a connection is lost the dialer waits at least this long before
        the first retry.  Subsequent retries use exponential back-off up to
        :attr:`reconnect_max_ms`.  Configured at construction time via
        :meth:`Socket.add_dialer`.

        Raises
        ------
        NngClosed
            If the dialer has been closed.
        """
        ...

    @property
    def recv_timeout(self) -> int:
        """
        Per-dialer receive timeout in milliseconds (-1 = infinite, 0 = non-blocking).

        Overrides the socket-level ``recv_timeout`` for pipes created by this
        dialer.  Configured at construction time via :meth:`Socket.add_dialer`.

        Raises
        ------
        NngClosed
            If the dialer has been closed.
        """
        ...

    @property
    def send_timeout(self) -> int:
        """
        Per-dialer send timeout in milliseconds (-1 = infinite, 0 = non-blocking).

        Overrides the socket-level ``send_timeout`` for pipes created by this
        dialer.  Configured at construction time via :meth:`Socket.add_dialer`.

        Raises
        ------
        NngClosed
            If the dialer has been closed.
        """
        ...

    @property
    def socket(self) -> Socket | None:
        """The parent :class:`Socket` for this dialer."""
        ...

    def close(self) -> None:
        """
        Close the dialer and tear down its active connection, if any.

        The pipe (connection) associated with this dialer is closed immediately.
        Any in-flight sends or receives on that pipe will fail.  The parent
        socket remains open and its other pipes are unaffected.

        If the dialer is already closed this method is a no-op (no exception
        is raised).
        """
        ...

    def start(self, *, block: bool = True) -> None:
        """
        Initiate the outbound connection.

        Must be called after :meth:`Socket.add_dialer` to actually begin
        connecting.  The dialer will maintain the connection and reconnect
        automatically if it is dropped.  Once started, the dialer's
        configuration is **locked** and cannot be changed.

        Parameters
        ----------
        block:
            If *True* (default) block until the first connection succeeds
            (raises on error).  If *False*, return immediately; nng retries
            in the background even if the initial attempt fails — errors
            such as ``NngConnectionRefused`` will **not** be raised in that
            case, and connection progress is opaque.

        Raises
        ------
        NngClosed
            If the dialer or socket have been closed.
        NngState
            If the dialer has already been started.
        NngConnectionRefused
            The remote peer refused the connection (blocking mode only).
        NngConnectionReset
            The remote peer reset the connection (blocking mode only).
        NngAuthError
            TLS peer authentication or authorization failed (blocking mode only).
        NngNoMemory
            Insufficient memory to complete the operation.
        NngError
            Other transport-level errors (e.g. ``NNG_EUNREACHABLE``,
            ``NNG_EPROTO``).
        """
        ...

    def __enter__(self) -> Self:
        ...

    def __exit__(self, *_: Any) -> None:
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class Listener:
    """
    An inbound endpoint that accepts connections on behalf of a :class:`Socket`.

    Unlike a :class:`Dialer` (which maintains a single outbound connection),
    a ``Listener`` can accept **any number of simultaneous inbound connections**.
    Each accepted connection becomes a *pipe* in the parent socket's pipe pool.

    A single socket can have **multiple listeners**, each bound to a different
    address or transport.  For example, a server can listen on both TCP and IPC
    at the same time, and all clients are handled through the same socket.

    Returned by :meth:`Socket.add_listener` (not yet started; call
    :meth:`start` to begin accepting connections).

    All configuration options (TLS, per-pipe timeouts) are set via
    :meth:`Socket.add_listener` before the listener is started.  Once
    :meth:`start` is called, the listener's configuration is **locked** and
    individual options can no longer be changed.

    .. note::
        The dialer/listener distinction is **orthogonal** to the
        client/server role in the protocol.  For example, a ``REQ`` socket
        may use a listener to accept connections from ``REP`` dialers.

    The listener is closed automatically when its parent socket is closed.
    It is also closed explicitly via :meth:`close` or when used as a
    context manager.

    The properties on this class are **read-only** and exist solely for
    inspection after the fact.

    Use as a context manager to stop and clean up the listener::

        with sock.add_listener("tcp://0.0.0.0:5555", recv_timeout=5000) as lst:
            lst.start()
            ...
    """
    @property
    def id(self) -> int:
        """The numeric listener ID, or 0 if the listener has been closed."""
        ...

    @property
    def pipes(self) -> list[Pipe]:
        """The active :class:`Pipe`(s) for this listener."""
        ...

    @property
    def recv_timeout(self) -> int:
        """
        Per-listener receive timeout in milliseconds (-1 = infinite, 0 = non-blocking).

        Overrides the socket-level ``recv_timeout`` for pipes accepted by this
        listener.  Configured at construction time via :meth:`Socket.add_listener`.

        Raises
        ------
        NngClosed
            If the listener has been closed.
        """
        ...

    @property
    def send_timeout(self) -> int:
        """
        Per-listener send timeout in milliseconds (-1 = infinite, 0 = non-blocking).

        Overrides the socket-level ``send_timeout`` for pipes accepted by this
        listener.  Configured at construction time via :meth:`Socket.add_listener`.

        Raises
        ------
        NngClosed
            If the listener has been closed.
        """
        ...

    @property
    def socket(self) -> Socket | None:
        """The parent :class:`Socket` for this listener."""
        ...

    def close(self) -> None:
        """
        Stop accepting new connections and close all pipes this listener created.

        Already-established pipes that are actively transferring data will be
        closed immediately.  The parent socket remains open and any pipes from
        *other* listeners or dialers continue operating normally.

        If the listener is already closed this method is a no-op (no exception
        is raised).
        """
        ...

    def start(self) -> None:
        """
        Bind to the address and start accepting inbound connections.

        Must be called after :meth:`Socket.add_listener` to actually begin
        accepting connections.  Once started, the listener accepts unlimited
        simultaneous peers until :meth:`close` is called.  The listener's
        configuration is **locked** after this call and cannot be changed.

        Raises
        ------
        NngClosed
            If the listener or socket have been closed.
        NngState
            If the listener has already been started.
        NngAddressInUse
            The address is already in use by another listener.
        NngNoMemory
            Insufficient memory to complete the operation.
        NngError
            Other transport-level errors (e.g. permission denied binding
            to a privileged port).
        """
        ...

    def __enter__(self) -> Self:
        ...

    def __exit__(self, *_: Any) -> None:
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class Context:
    """
    A protocol context (nng_ctx).

    Provides an independent request/reply state machine on a shared socket,
    allowing many concurrent in-flight requests without raw mode::

        sock = ReqSocket()
        sock.add_dialer("tcp://127.0.0.1:5000").start()
        ctx  = sock.open_context()
        ctx.send_msg(Message(b"ping"))
        reply = ctx.recv_msg()
        ctx.close() # optional
    """
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        ...

    @property
    def id(self) -> int:
        ...

    @property
    def recv_timeout(self) -> int:
        """Receive timeout in ms (-1 = infinite, 0 = non-blocking)."""
        ...
    @recv_timeout.setter
    def recv_timeout(self, value: int) -> None: ...

    @property
    def send_timeout(self) -> int:
        """Send timeout in ms."""
        ...
    @send_timeout.setter
    def send_timeout(self, value: int) -> None: ...

    @property
    def socket(self) -> Socket | None:
        """The parent Socket for this context."""
        ...

    async def arecv(self) -> Message:
        """Receive a Message asynchronously through this context."""
        ...

    async def asend(self, data: bytes | Message | Buffer | bytearray | str) -> None:
        """Send a Message asynchronously through this context."""
        ...

    def close(self) -> None:
        ...

    def recv(self, *, nonblock: bool = False) -> Message:
        """Receive a Message through this context."""
        ...

    def send(self, data: bytes | Message | Buffer | bytearray | str, *, nonblock: bool = False) -> None:
        """Send a Message through this context (transfers ownership)."""
        ...

    def submit_recv(self) -> concurrent.futures.Future[Message]:
        """
        Receive asynchronously; return a concurrent.futures.Future → bytes.

        The future resolves to the received payload as ``bytes``.
        Can be called from any thread without a running event loop.
        """
        ...

    def submit_send(self, data: bytes | Message | Buffer | bytearray | str) -> concurrent.futures.Future[None]:
        """
        Send *data* (bytes-like) asynchronously; return a concurrent.futures.Future.

        The future resolves to ``None`` on success or raises an :exc:`NngError`
        on failure.  Can be called from any thread without a running event loop.
        """
        ...

    def __enter__(self) -> Self:
        ...

    def __exit__(self, *_: Any) -> None:
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class Socket:
    """
    Base class for all nng protocol sockets.

    **Connections and pipes**

    A socket does not correspond to a single connection.  Instead it owns a
    *pool of pipes*, where each pipe is one physical connection:

    * Call :meth:`add_dialer` one or more times to add outbound connections.
      Each call creates and stores a :class:`Dialer`; call
      :meth:`Dialer.start` on it to initiate the connection.  A dialer
      manages one pipe and reconnects automatically if the connection drops.
    * Call :meth:`add_listener` one or more times to accept inbound
      connections.  A single :class:`Listener` can accept unlimited
      simultaneous peers; call :meth:`Listener.start` on it to begin
      accepting.
    * Both can be combined freely on the same socket.
    * The socket keeps strong references to all dialers and listeners it
      creates, so they are not garbage-collected while the socket is open.

    **How messages are routed across pipes**

    This is entirely determined by the protocol subclass:

    * :class:`PubSocket` — broadcast to *all* pipes.
    * :class:`PushSocket` — round-robin / load-balance across pipes.
    * :class:`ReqSocket` — send to *one* available pipe; reply must arrive on
      the same pipe.  Uses the first ready pipe, not strict round-robin.
    * :class:`PairSocket` — one active pipe at a time
    * :class:`BusSocket` — send to *all other* peers.
    * :class:`SurveyorSocket` — broadcast survey; collect all responses.

    ** Thread-safety and concurrency **
    All socket methods are thread-safe and can be called concurrently from
    multiple threads.
    `submit_send`, `submit_recv`, `asend`, and `arecv` internally submit an
    asynchronous operation to the socket's I/O thread and return a Future,
    avoiding to block until completion. They can be called concurrently
    with each other and with `recv`/`send`, as well as from different
    threads or asyncio loops.

    **Available transports**

    The URL scheme passed to :meth:`add_dialer` / :meth:`add_listener` selects
    the transport:

    * **TCP** – ``tcp://<host>:<port>``

      Cross-machine communication over TCP/IP.  Use ``tcp4://`` or ``tcp6://``
      to force IPv4 or IPv6.  When listening, omit the host or use ``0.0.0.0``
      / ``::`` to accept on all interfaces, e.g. ``tcp://0.0.0.0:5555`` or
      simply ``tcp://:5555``.  IPv6 literals must be bracketed:
      ``tcp://[::1]:5555``.

    * **IPC** – ``ipc://<path>``

      Inter-process communication on the same host.  Uses UNIX domain sockets
      on POSIX and named pipes on Windows.  On POSIX, prefer absolute paths to
      avoid working-directory sensitivity.  ``unix://`` is an alias for
      ``ipc://`` on POSIX systems.

    * **Abstract** (Linux only) – ``abstract://<name>``

      Linux abstract-namespace sockets.  Names are URI-encoded and do not
      appear in the filesystem; they are freed automatically when no longer in
      use.  Embedded NUL bytes can be encoded as ``%00``, e.g.
      ``abstract://foo%00bar``.

    * **In-process** – ``inproc://<name>``

      Zero-copy communication between sockets **within the same process**.
      The name is an arbitrary string; different names are completely
      independent. Sockets in separate processes, or using different NNG
      library instances, cannot communicate via inproc.

    * **UDP** *(experimental)* – ``udp://<host>:<port>``

      Unreliable, connectionless transport.  Messages are ordered by nng but
      gaps are possible; there is no delivery guarantee.  Maximum message size
      is effectively ~1140 bytes on standard Ethernet (65 000 B hard cap).
      Use ``udp4://`` or ``udp6://`` to restrict to a specific IP version.
      IPv6 literals must be bracketed: ``udp://[::1]:5555``.

    **Closing**

    Closing a socket (via :meth:`close` or exiting a ``with`` block) waits for
    outstanding operations to finish or be aborted.  All associated dialers,
    listeners, pipes, and contexts are invalidated.

    **Typical usage**::

        with ReqSocket() as sock:
            sock.add_dialer("tcp://127.0.0.1:5555").start()
            sock.send(b"ping")
            reply = sock.recv()
    """
    @property
    def id(self) -> int:
        """The underlying nng socket ID (positive) or 0 if closed."""
        ...

    @property
    def peer_name(self) -> str:
        """Peer protocol name."""
        ...

    @property
    def pipes(self) -> list[Pipe]:
        """
        Snapshot list of currently live :class:`Pipe` objects.

        The list is a copy; it will not change as connections are added or
        removed.  Inspect :attr:`Pipe.status` for the state of each pipe.
        """
        ...

    @property
    def proto_name(self) -> str:
        """Protocol name (e.g. ``"pair1"``)."""
        ...

    @property
    def recv_buf(self) -> int:
        """Receive buffer depth (number of queued messages)."""
        ...
    @recv_buf.setter
    def recv_buf(self, value: int) -> None: ...

    @property
    def recv_fd(self) -> int:
        """
        Readable file descriptor for external poll / epoll / kqueue.

        Becomes readable when the socket has data to receive.
        Never read or write to this fd directly.
        """
        ...

    @property
    def recv_max_size(self) -> int:
        """Maximum incoming message size in bytes (0 = no limit)."""
        ...
    @recv_max_size.setter
    def recv_max_size(self, value: int) -> None: ...

    @property
    def recv_timeout(self) -> int:
        """Receive timeout in ms (-1 = infinite, 0 = non-blocking)."""
        ...
    @recv_timeout.setter
    def recv_timeout(self, value: int) -> None: ...

    @property
    def send_buf(self) -> int:
        """Send buffer depth (number of queued messages)."""
        ...
    @send_buf.setter
    def send_buf(self, value: int) -> None: ...

    @property
    def send_fd(self) -> int:
        """
        Writable file descriptor for external poll / epoll / kqueue.

        Becomes writable when the socket can accept a send.
        """
        ...

    @property
    def send_timeout(self) -> int:
        """Send timeout in ms."""
        ...
    @send_timeout.setter
    def send_timeout(self, value: int) -> None: ...

    def add_dialer(self, url: str | bytes, *, tls: TlsConfig | None = None, reconnect_min_ms: int | None = None, reconnect_max_ms: int | None = None, recv_timeout: int | None = None, send_timeout: int | None = None) -> Dialer:
        """
        Create, configure, and register an outbound :class:`Dialer` for *url*.

        The dialer is created and all options applied, but the connection is
        **not** started yet.  Call :meth:`Dialer.start` on the returned object
        to initiate the connection::

            dialer = sock.add_dialer("tcp://127.0.0.1:5555",
                                     reconnect_min_ms=100)
            dialer.start()          # blocks until first connection succeeds

            # or as a one-liner:
            sock.add_dialer("tcp://127.0.0.1:5555").start()

        The socket keeps a strong reference to the returned :class:`Dialer`;
        it will not be garbage-collected while the socket is open.

        Each call registers **one** outbound connection slot.  Call
        :meth:`add_dialer` multiple times to connect to several peers; the
        protocol layer pools all resulting pipes and distributes messages
        across them.

        Parameters
        ----------
        url:
            Connection URL, e.g. ``"tcp://127.0.0.1:5555"``.
        tls:
            A :class:`TlsConfig` for TLS connections.  The object must remain
            alive for the lifetime of the dialer.
        reconnect_min_ms:
            Minimum reconnect interval in ms (default: 100).  After a
            disconnect the dialer waits at least this long before retrying.
        reconnect_max_ms:
            Maximum reconnect interval in ms.  Retries use exponential
            back-off capped at this value.  ``0`` (default) means use
            *reconnect_min_ms* as a fixed interval.
        recv_timeout:
            Receive timeout in ms for the pipe created by this dialer
            (overrides the socket-level value).  ``-1`` = infinite,
            ``0`` = non-blocking.
        send_timeout:
            Send timeout in ms for the pipe created by this dialer
            (overrides the socket-level value).

        Raises
        ------
        NngClosed
            If the socket has been closed.
        NngNotSupported
            If *url* uses a transport scheme that is not recognized by nng
            (e.g. ``"unknown://host:1234"``).  Note: nng raises NngNotSupported
            rather than NngInvalidArgument for unrecognized transport names.
        NngInvalidArgument
            If *url* is otherwise syntactically invalid for a known transport.
        """
        ...

    def add_listener(self, url: str | bytes, *, tls: TlsConfig | None = None, recv_timeout: int | None = None, send_timeout: int | None = None) -> Listener:
        """
        Create, configure, and register an inbound :class:`Listener` for *url*.

        The listener is created and all options applied, but it does **not**
        start accepting connections yet.  Call :meth:`Listener.start` on the
        returned object to begin listening::

            lst = sock.add_listener("tcp://0.0.0.0:5555", recv_timeout=5000)
            lst.start()

            # or as a one-liner:
            sock.add_listener("tcp://0.0.0.0:5555").start()

        The socket keeps a strong reference to the returned :class:`Listener`;
        it will not be garbage-collected while the socket is open.

        A listener accepts **unlimited** simultaneous inbound connections, each
        of which becomes a pipe in the socket's pool.  Call
        :meth:`add_listener` multiple times to accept on several addresses or
        transports at once.

        Parameters
        ----------
        url:
            Address to bind to, e.g. ``"tcp://0.0.0.0:5555"``.
        tls:
            A :class:`TlsConfig` for TLS connections.  The object must remain
            alive for the lifetime of the listener.
        recv_timeout:
            Receive timeout in ms for pipes accepted by this listener
            (overrides the socket-level value).  ``-1`` = infinite,
            ``0`` = non-blocking.
        send_timeout:
            Send timeout in ms for pipes accepted by this listener
            (overrides the socket-level value).

        Raises
        ------
        NngClosed
            If the socket has been closed.
        NngNotSupported
            If *url* uses a transport scheme that is not recognized by nng
            (e.g. ``"unknown://host:1234"``).  Note: nng raises NngNotSupported
            rather than NngInvalidArgument for unrecognized transport names.
        NngInvalidArgument
            If *url* is otherwise syntactically invalid for a known transport.
        NngAddressInUse
            If the address is already bound by another listener.
        """
        ...

    async def arecv(self) -> Message:
        """Receive bytes asynchronously (asyncio-compatible)."""
        ...

    def arecv_ready(self) -> AsyncGenerator[None, None]:
        """
        Async generator that yields each time the socket has a message ready.

        ``recv_fd`` is registered with the event loop *once* for the lifetime of
        the generator — ``add_reader`` / ``remove_reader`` are each called only
        once regardless of how many messages are awaited.  Epoll fires directly
        in the event loop thread with no nng thread pool and no
        ``call_soon_threadsafe`` round-trip.

        After each ``yield``, a call to :meth:`recv` / :meth:`recv_msg` with
        ``nonblock=True`` is guaranteed to succeed immediately::

            watcher = sock.arecv_ready()
            async for _ in watcher:
                data = sock.recv(nonblock=True)
                # or equivalently:
                # await watcher.__anext__()
                # data = sock.recv(nonblock=True)
            await watcher.aclose()
        """
        ...

    async def asend(self, data: bytes | Message | Buffer | bytearray | str) -> None:
        """
        Send bytes asynchronously (asyncio-compatible).

        NOTE: if the coroutine is cancelled while waiting for send
        completion, the data may or may not have been sent.
        """
        ...

    def asend_ready(self) -> AsyncGenerator[None, None]:
        """
        Async generator that yields each time the socket can accept a send.

        ``send_fd`` is registered with the event loop *once* for the lifetime of
        the generator — ``add_reader`` / ``remove_reader`` are each called only
        once regardless of how many sends are awaited.  Epoll fires directly in
        the event loop thread with no nng thread pool and no
        ``call_soon_threadsafe`` round-trip.

        After each ``yield``, a call to :meth:`send` / :meth:`send_msg` with
        ``nonblock=True`` is guaranteed to succeed immediately.

        For REP sockets the fd becomes readable as soon as nng has finished
        sending the previous reply — making this the ideal low-latency
        synchronisation point before a non-blocking send::

            recv_watcher = sock.arecv_ready()
            send_watcher = sock.asend_ready()
            async for _ in recv_watcher:
                msg = sock.recv(nonblock=True)
                await send_watcher.__anext__()
                sock.send(reply, nonblock=True)
            await send_watcher.aclose()
        """
        ...

    def close(self) -> None:
        """
        Close the socket and release all resources.

        Waits for any outstanding operations to complete or be aborted before
        returning.  All associated :class:`Dialer`, :class:`Listener`,
        :class:`Pipe`, and :class:`Context` objects are invalidated.

        .. note::
            This method blocks until in-flight operations finish.  Do not call
            it from a context that cannot block.  Closing the socket may
            disrupt transfers that are still in progress.
        """
        ...

    def on_new_pipe(self, callback: Callable[[Pipe], None] | None) -> None:
        """
        Register a callback invoked when a new connection is negotiated.

        The callback is called at :attr:`PipeStatus.ADDING` – before the pipe
        is admitted to the socket pool.  Calling :meth:`Pipe.close` inside
        the callback rejects the connection entirely.

        Only one callback is kept; calling this method again replaces the
        previous one.  Pass ``None`` to disable.

        Parameters
        ----------
        callback:
            ``(pipe: Pipe) -> None``.  Executed on the dispatcher thread;
            keep it short and non-blocking.
        """
        ...

    def recv(self, *, nonblock: bool = False) -> Message:
        """
        Receive and return a :class:`Message` (zero-copy via buffer protocol).

        Parameters
        ----------
        nonblock:
            If *True*, raise :exc:`NngAgain` if no message is ready.
        """
        ...

    def send(self, data: bytes | Message | Buffer | bytearray | str, *, nonblock: bool = False) -> None:
        """
        Send an :class:`Message`.  Transfers ownership on success.

        Parameters
        ----------
        data:
            The :class:`Message` (or bytes-like) to send.
        nonblock:
            If *True*, raise :exc:`NngAgain` instead of blocking.
        """
        ...

    def submit_recv(self) -> concurrent.futures.Future[Message]:
        """
        Receive asynchronously; return a concurrent.futures.Future → bytes.

        The future resolves to the received payload as ``bytes``.
        Can be called from any thread.
        """
        ...

    def submit_send(self, data: bytes | Message | Buffer | bytearray | str) -> concurrent.futures.Future[None]:
        """
        Send *data* (bytes-like) asynchronously; return a concurrent.futures.Future.

        The future resolves to ``None`` on success or raises an :exc:`NngError`
        on failure. Can be called from any thread.
        """
        ...

    def __enter__(self) -> Self:
        ...

    def __exit__(self, *_: Any) -> None:
        ...

    def __repr__(self) -> str:
        """Return repr(self)."""
        ...


class PairSocket(Socket):
    """
    Bidirectional, point-to-point socket (Pair protocol).

    A pair socket can send and receive freely in both directions.  In standard
    mode it is strictly **one-to-one**: only a single pipe is active at a time.
    Messages sent while no pipe is connected are held in the send buffer.

    .. note::
       PAIR does **not** guarantee reliable delivery.  Although back-pressure
       prevents most discards, some topologies can still lose messages.
       Applications that need reliable delivery should use :class:`ReqSocket`
       or add their own acknowledgment layer.

    **Protocol versions**

    The default is Pair v1.  Pass ``v0=True`` for the legacy v0 protocol, which
    lacks hop-count headers and is required when interoperating with older
    implementations such as *libnanomsg* or *mangos*.

    Parameters
    ----------
    v0:
        Use the older Pair v0 protocol instead of the default v1.
    """
    ...

class PubSocket(Socket):
    """
    Publish socket — broadcasts every message to **all** connected subscribers.

    A message sent on a ``PubSocket`` is delivered to every :class:`SubSocket`
    peer that has a matching subscription prefix.  Peers with no matching
    subscription silently discard the message.

    * **Fan-out**: one send → copied to all active pipes.
    * **Topic filtering**: filtering happens on the *subscriber* side based on the
      message body prefix (see :meth:`SubSocket.subscribe`).  The publisher
      always sends every message to every subscriber; subscriptions do **not**
      reduce network bandwidth.
    * **No back-pressure**: a slow subscriber does not block the publisher;
      messages for that subscriber are dropped when its send buffer is full.
    * PubSocket cannot receive; :meth:`recv` raises ``NngNotSupported``.
    """
    ...

class SubSocket(Socket):
    """
    Subscribe socket — receives messages from a :class:`PubSocket`.

    By default a ``SubSocket`` receives **nothing**.  You must subscribe to at
    least one topic prefix with :meth:`subscribe` before any messages arrive.
    The socket can hold multiple subscriptions simultaneously.

    Messages whose body does *not* start with at least one registered prefix
    are silently dropped by nng before they reach the application.  Topic
    filtering is done **locally** on the subscriber side — the publisher always
    sends every message to every subscriber, so subscriptions do not reduce
    bandwidth consumption.

    * SubSocket is receive-only; :meth:`send` raises ``NngNotSupported``.
    * One ``SubSocket`` can connect to multiple publishers by calling
      :meth:`add_dialer` several times.
    * When the receive queue is full, **newer** messages are kept and older
      unread messages are discarded by default.

    Example::

        sub = SubSocket()
        sub.add_dialer("tcp://127.0.0.1:5556").start()
        sub.subscribe(b"")       # receive every message
        sub.subscribe(b"news.")  # also accept prefix-matched topics
        msg = sub.recv()
    """
    @property
    def skip_older_on_full_queue(self) -> Any:
        """
        Whether to drop older messages when the receive queue is full.

        By default, when the receive queue is full, newer messages are kept and
        older unread messages are dropped.  Setting this option to ``False``
        reverses that behavior: older messages are kept and newer messages are
        dropped instead.
        """
        ...

    def open_context(self) -> Context:
        """Open an independent sub context on this socket."""
        ...

    def subscribe(self, prefix: bytes = b'') -> None:
        """
        Subscribe to messages whose body starts with *prefix*.

        An empty prefix subscribes to all messages.
        """
        ...

    def unsubscribe(self, prefix: bytes = b'') -> None:
        """Remove a topic subscription."""
        ...


class ReqSocket(Socket):
    """
    Request socket — sends a request and waits for exactly one reply.

    The REQ/REP protocol enforces a strict send → receive → send → … cycle on
    each context.  Violating this order (e.g. two sends in a row) raises an
    error.

    **Message routing with multiple peers**

    When the socket has several pipes (e.g. after multiple :meth:`add_dialer`
    calls or when connected to a server with multiple workers via contexts),
    each request is sent to the **first available** pipe — whichever peer is
    ready to accept it.  The reply is expected to arrive on the *same* pipe.

    If the pipe dies before the reply arrives, the request is automatically
    resent on another available pipe (see :attr:`resend_time`).  Because
    requests may be retransmitted, they should be **idempotent** — executing
    the same request twice must produce the same result.  If that is not
    possible, set :attr:`resend_time` to ``-1`` to disable automatic resending.

    Cancelling a pending request is done by sending a new request; any reply
    from the cancelled request that arrives later is silently discarded.

    **Concurrent requests**

    A bare ``ReqSocket`` allows only one in-flight request at a time.  To
    issue multiple concurrent requests, open independent contexts with
    :meth:`open_context`.  Each context runs its own REQ state machine and can
    be in-flight on a different pipe simultaneously.
    """
    @property
    def resend_tick(self) -> Any:
        """
        How often (ms) to check for timed-out requests to resend.

        The socket checks for timed-out requests at this interval.
        Lower values add overhead, but are needed for short resend_time values.

        The default is 1000 ms.
        """
        ...

    @property
    def resend_time(self) -> int:
        """
        How long (ms) to wait for a reply before resending the request.

        Requests are automatically resent if the peer disconnects.

        resend_time adds a timer at which requests will be resent (even if no
        disconnection has been detected) to the peer until a reply is received.

        ``-1`` (the default) disables automatic resending (exception on disconnect)
        Setting a positive value provides automatic fault-tolerance when working with
        multiple REP peers, or possible packet loss.

        Setting a < 1000ms resend_time requires tuning :attr:`resend_tick` to
        have real effect.
        """
        ...
    @resend_time.setter
    def resend_time(self, value: int) -> None: ...

    def open_context(self) -> Context:
        """
        Open an independent REQ context on this socket.

        Each context has its own send → receive → send state machine and can
        have a request in-flight on a different pipe at the same time as other
        contexts.  This is the preferred way to pipeline concurrent requests
        without opening multiple sockets.
        """
        ...


class RepSocket(Socket):
    """
    Reply socket — receives a request and sends exactly one reply.

    The REP/REQ protocol enforces a strict receive → send → receive → … cycle
    on each context.  Attempting to send a reply before receiving a request, or
    to receive a second request before replying to the first, raises
    ``NngState``.

    **Serving multiple concurrent clients**

    A single ``RepSocket`` can be connected to any number of request senders
    (via :meth:`add_listener` or :meth:`add_dialer`).  However, the
    socket-level :meth:`recv` / :meth:`send` cycle handles only **one request
    at a time** — only one receive operation may be pending simultaneously.
    To serve multiple clients concurrently without blocking, open independent
    per-client contexts with :meth:`open_context`; each context carries the
    reply-routing state for one in-flight request.
    """
    def open_context(self) -> Context:
        """
        Open an independent REP context on this socket.

        Each context can receive one request and send one reply independently
        of all other contexts.  Use this to serve multiple clients concurrently
        from a single socket without a thread per client.
        """
        ...


class PushSocket(Socket):
    """
    Push socket — distributes messages across connected pull peers (pipeline).

    PUSH/PULL implements a fan-out work queue.  Each message sent on a
    ``PushSocket`` is delivered to **exactly one** :class:`PullSocket` peer,
    chosen by a fair round-robin / load-balancing algorithm that favours peers
    with available buffer space.

    * Messages are never duplicated — each goes to one worker.
    * If there are no connected peers the send blocks (or raises
      :exc:`NngAgain` in non-blocking mode) until a peer is available.  Back-
      pressure is honoured: only peers capable of accepting a message are
      considered.
    * PushSocket is send-only; :meth:`recv` raises ``NngNotSupported``.
    * **No delivery guarantee**: there is no acknowledgment mechanism.  If
      reliable delivery is required, consider :class:`ReqSocket` instead.
    """
    @property
    def send_buffer_length(self) -> Any:
        """
        Maximum number of messages that can be buffered for sending to a peer.

        Default (0), means operations are unbuffers, and sending will block
        until a peer is available to receive the message.

        Setting a positive value queues to an intermediate buffer of that many
        messages.

        The maximum value is 8192.
        """
        ...


class PullSocket(Socket):
    """
    Pull socket — receives work items distributed by a :class:`PushSocket`.

    Each message arrives from exactly one push sender and is delivered to
    exactly this socket (not duplicated to others).  Multiple ``PullSocket``
    instances connected to the same ``PushSocket`` act as a pool of workers:
    nng load-balances across them automatically.

    When two peers both have a message ready simultaneously, the order in which
    messages are delivered to the application is undefined.

    * PullSocket is receive-only; :meth:`send` raises ``NngNotSupported``.
    """
    ...

class SurveyorSocket(Socket):
    """
    Surveyor socket — broadcasts a survey and collects responses within a deadline.

    Sending a message broadcasts it to **all** connected
    :class:`RespondentSocket` peers.  Respondents are not obliged to reply;
    the surveyor collects as many responses as arrive within :attr:`survey_time`
    milliseconds.  After the deadline expires, further :meth:`recv` calls raise
    :exc:`NngError` with ``NngTimeout`` and :meth:`recv` before any survey
    has been sent raises ``NngState``.

    Sending a new survey **cancels** the previous one; any responses from the
    earlier survey that arrive afterwards are silently discarded.

    Each connected respondent normally sends at most one reply per survey,
    though duplicates are possible in some topologies.

    Useful for solving voting / leader-election and service-discovery problems.

    Each send opens a new survey; the previous survey's results are discarded.
    Use :meth:`open_context` to run multiple independent surveys concurrently;
    each context maintains its own deadline and does not cancel surveys on
    other contexts.
    """
    @property
    def survey_time(self) -> int:
        """
        Survey deadline in milliseconds.

        After sending a survey, the socket accepts responses for this long.
        Once the deadline expires, :meth:`recv` raises :exc:`NngError` with
        ``NngTimeout``.  The next :meth:`send` starts a fresh survey.
        """
        ...
    @survey_time.setter
    def survey_time(self, value: int) -> None: ...

    def open_context(self) -> Context:
        """Open an independent surveyor context on this socket."""
        ...


class RespondentSocket(Socket):
    """
    Respondent socket — receives surveys and sends back a single response each.

    The protocol is similar to REP: :meth:`recv` waits for an incoming survey,
    and :meth:`send` delivers the response back to the surveyor that sent it.
    Multiple respondents can reply to the same survey; the surveyor collects
    all replies within its deadline.

    A respondent may choose to discard a survey by simply not replying to it.
    Attempting to :meth:`send` without first receiving a survey raises
    ``NngState``.

    Useful for voting / leader-election and service-discovery problems.

    Use :meth:`open_context` to receive and respond to multiple surveys in
    parallel; each context has its own independent state machine.
    """
    def open_context(self) -> Context:
        """Open an independent respondent context on this socket."""
        ...


class BusSocket(Socket):
    """
    Bus socket — broadcasts each message to **all** other connected bus nodes.

    Every message sent on a ``BusSocket`` is forwarded to every other
    ``BusSocket`` peer **directly** connected to it.  Unlike PUB/SUB there is
    no topic filtering and the bus is symmetric: all peers can both send and
    receive.

    Typical use case: all-to-all communication where every node needs to hear
    about events from every other node.

    .. important::
       Messages are only delivered to **directly** connected peers.  Peers
       connected indirectly (e.g. via an intermediate node) will **not**
       receive them.  To achieve true all-to-all communication you must build
       a fully-connected mesh.

    .. note::
       BUS delivery is **best-effort**: sends never block.  If a peer cannot
       receive (e.g. its buffer is full), its copy of the message is silently
       discarded without affecting other peers.  The protocol is not suitable
       for high-throughput workloads where loss would be unacceptable.
    """
    ...

# ---------------------------------------------------------------------------
# Module-level constants
# ---------------------------------------------------------------------------

# Module-level constants

TLS_AUTH_NONE: int
TLS_AUTH_OPTIONAL: int
TLS_AUTH_REQUIRED: int
TLS_VERSION_1_2: int
TLS_VERSION_1_3: int

# ---------------------------------------------------------------------------
# Module-level functions
# ---------------------------------------------------------------------------

# Module-level functions

def version() -> Any:
    """Return the nng library version string (e.g. ``"2.0.0-dev"``)."""
    ...

def random() -> Any:
    """Return a 32-bit cryptographically random value."""
    ...

def initialize(*, num_task_threads: int = 0, max_task_threads: int = -1, num_expire_threads: int = 0, max_expire_threads: int = -1, num_poller_threads: int = 0, max_poller_threads: int = -1, num_resolver_threads: int = 0) -> Any:
    """
    Initialise the nng library with custom threading parameters.

    Must be called **before** creating any nng object or calling any nng
    function.  If not called explicitly, the library is auto-initialised with
    default parameters on first use.

    Parameters (all keyword-only, default 0 / -1 = nng default / no cap):
        num_task_threads     – fixed number of task-queue threads (0 = auto)
        max_task_threads     – cap on task-queue threads (-1 = no cap)
        num_expire_threads   – fixed number of expiry threads (0 = auto)
        max_expire_threads   – cap on expiry threads (-1 = no cap)
        num_poller_threads   – fixed number of I/O poller threads (0 = auto)
        max_poller_threads   – cap on poller threads (-1 = no cap)
        num_resolver_threads – fixed number of DNS resolver threads (0 = auto)

    Raises RuntimeError if nng is already initialised or if nng_init() fails.
    """
    ...
