Contents

jvdvleuten/phoenixnectar

Actor-owned runtime • typed push & event helpers • reconnect/rejoin • async streams

Requirements

| Platform | Minimum | | --------- | ------- | | Swift | 6.1+ | | iOS | 16+ | | macOS | 15+ | | tvOS | 18+ | | watchOS | 11+ |

Install

.package(url: "https://github.com/jvdvleuten/PhoenixNectar.git", from: "0.1.0")

Quickstart

import PhoenixNectar

struct ChatMessage: Codable, Sendable {
  let body: String
}

let client = try Socket(endpoint: "wss://api.example.com/socket")

try await client.connect()

let channel = try await client.joinChannel("room:lobby")

// Subscribe to inbound events
let posted = Event<ChatMessage>("message:posted")
Task {
  for try await message in await channel.subscribe(to: posted) {
    print(message.body)
  }
}

// Push a typed event and await the reply
let send = Push<ChatMessage, ChatMessage>("message:send")
let reply = try await channel.push(send, payload: ChatMessage(body: "hello"))
print(reply.body)

For most apps, this is enough:

  1. connect()
  2. joinChannel("room:lobby")
  3. subscribe(to: Event<T>)
  4. push(Push<Req, Resp>, payload: ...)

Use joinChannel(..., params: ...) only when your server expects a phx_join payload. Use authTokenProvider and connectParamsProvider only when your socket connect flow needs them.


Contents

Why This Library

  • async/await instead of callback-heavy channel APIs
  • Typed Push and Event helpers for compile-time safety
  • Actor-owned runtime state with Swift 6 strict concurrency
  • Reconnect, heartbeat, and automatic rejoin built in
  • Compact public API with lower-level APIs still available when needed

Core Behavior

| Behavior | Detail | | --- | --- | | Provider closures | Re-evaluated on connect and reconnect | | Transport loss | Triggers reconnect with configured backoff | | Rejoin | Joined topics are automatically rejoined after reconnect | | Heartbeat timeout | Follows the same reconnect path | | Clean close (1000) | Reconnects by default; disable with reconnectOnCleanClose: false | | Explicit disconnect() | Treated as intentional — no auto-reconnect | | State observation | connectionStateStream() emits lifecycle transitions |

Authentication and Join Params

| Parameter | Purpose | | --- | --- | | authTokenProvider | Phoenix auth tokens — re-evaluated on each connect/reconnect | | connectParamsProvider | Socket-level metadata (device ID, locale, etc.) | | joinChannel(..., params:) | Channel-specific phx_join payloads only |

Connection State

for await state in await client.connectionStateStream() {
  print(state) // .connecting, .connected, .disconnected(reason)
}

Raw Replies and Binary Pushes

Use typed Push/Event first. Drop to raw replies only when needed:

let reply = try await channel.push("message:send", payload: ["body": "hello"])
guard reply.status == .ok else {
  throw PhoenixError.serverError(reply)
}

Binary channel payloads:

let reply = try await channel.pushBinary("binary:upload", data: bytes)

Demo

Start the local Phoenix backend:

./scripts/run-demo-backend.sh

Then:

  • Open http://127.0.0.1:4000/chat_demo.html for the browser demo
  • Open demos/ChatShowcaseApp/ChatShowcaseApp.xcodeproj for the iOS demo

Default demo endpoint: ws://127.0.0.1:4000/socket

Docs

| Document | Description | | --- | --- | | API Reference | Full public API with signatures and examples | | Examples | Short reference snippets for common tasks | | E2E Harness | Phoenix backend for integration testing | | Contributing | Development setup and PR guidelines | | Changelog | Version history |

Package Metadata

Repository: jvdvleuten/phoenixnectar

Default branch: main

README: README.md