Skip to content

Swift: Replace Session static method zoo with a fluent builder API (mirror Rust) #281

@fasterthanlime

Description

@fasterthanlime

Background

The Rust vox-core API uses a clean fluent builder pattern for session establishment:

// Initiator
let client: EchoClient = initiator_on(link, TransportMode::Bare)
    .metadata(metadata)
    .on_connection(my_acceptor)
    .keepalive(SessionKeepaliveConfig { ... })
    .resumable()
    .establish::<EchoClient>()
    .await?;

// Acceptor
let guard = acceptor_on(link)
    .on_connection(my_acceptor)
    .resumable()
    .session_registry(registry.clone())
    .establish::<NoopClient>()
    .await?;

Two entry points (initiator_on / acceptor_on), each returning a builder. Options are set by chaining methods. The terminal method is always .establish() (or .establish_or_resume() on the acceptor side).

Current Swift situation

Swift has a Session class with 14+ static factory methods covering every combination of:

  • Input type: SessionConnector vs raw Link vs raw LinkAttachment
  • Typed vs untyped ExpectedRootClient
  • Fresh establish vs establish-or-resume

This is the combinatorial explosion that the Rust builder pattern avoids entirely.

// Current — pick from 14 overloads
Session.acceptor(connector, dispatcher: d, onConnection: acc, resumable: true, metadata: m)
Session.acceptorOrResume(connector, registry: r, dispatcher: d, ...)
Session.acceptFreshLink(link, conduit: .bare, dispatcher: d, ...)
Session.acceptFreshLinkOrResume(link, conduit: .bare, registry: r, dispatcher: d, ...)
Session.acceptFreshAttachment(attachment, conduit: .bare, dispatcher: d, ...)
Session.acceptFreshAttachmentOrResume(attachment, conduit: .bare, registry: r, ...)
// ... and 8 more typed/untyped variants of the above

Proposed Swift builder API

// Entry points (free functions, like Rust)
public func initiator(_ source: some LinkSource) -> InitiatorBuilder
public func acceptor(_ source: some LinkSource) -> AcceptorBuilder

// Chainable options (both builders)
.dispatcher(_ d: any ServiceDispatcher) -> Self
.onConnection(_ a: any ConnectionAcceptor) -> Self   // already implemented!
.keepalive(_ k: SessionKeepaliveConfig) -> Self
.resumable() -> Self
.metadata(_ m: [MetadataEntry]) -> Self

// Acceptor-only
.registry(_ r: SessionRegistry) -> Self

// Terminal
.establish() async throws -> Session
.establishOrResume() async throws -> SessionAcceptOutcome  // acceptor only

Call sites would become:

// Subject server
let session = try await acceptor(TcpAcceptor(host: host, port: port))
    .dispatcher(TestbedDispatcher(handler: handler))
    .onConnection(DefaultConnectionAcceptor(dispatcher: ...))
    .resumable()
    .establish()

// Subject client
let session = try await initiator(TcpConnector(host: host, port: port))
    .dispatcher(TestbedDispatcher(handler: handler))
    .resumable()
    .establish()

Input type unification

The three input types (SessionConnector, raw Link, raw LinkAttachment) can be unified by making entry points accept some LinkSourceTcpConnector, TcpAcceptor, UnixConnector all already conform to LinkSource. Raw links can be wrapped with the existing singleLinkSource(_:) helper.

What's already done (prerequisites merged)

  • ConnectionAcceptor / ConnectionRequest / PendingConnection protocol added (commit 1c633bd)
  • acceptConnections: Bool replaced with onConnection: (any ConnectionAcceptor)?
  • NoopClient (was NoopRootClient), SessionKeepaliveConfig (was DriverKeepaliveConfig) renamed
  • establishInitiator/establishAcceptor made internal
  • TcpAcceptor added to VoxRuntime

What this issue tracks

  • Design and implement InitiatorBuilder and AcceptorBuilder structs in VoxRuntime
  • Replace the 14+ Session static factory methods with the two builder entry points
  • Update all call sites in Subject.swift, Server.swift, and tests
  • Delete the now-redundant Session static methods (or deprecate them if source compatibility matters)
  • The Session class itself stays — it is the result of .establish() and holds rootConnection, driver, handle, peerMetadata, and run()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions