Transport-agnostic primitives for implementing Swift Distributed Actor systems.
swift-actor-runtime provides the foundational building blocks needed by any distributed actor system implementation, regardless of transport layer (BLE, gRPC, HTTP/2, WebSocket, etc.).
Vision: "Write once, run on any transport"
| Audience | Use Case |
|---|---|
| Transport Authors | Building a new distributed actor transport (e.g., MQTT, WebSocket, custom protocol). This library provides all the common infrastructure so you can focus on connectivity. |
| App Developers | Using an existing transport (Bleu, ActorEdge). You don't need to use this library directly—it's a dependency of your transport. |
Use this library if you are:
- Implementing a new
DistributedActorSystemfor a specific protocol - Building infrastructure that needs to serialize/deserialize distributed actor calls
- Creating a transport-agnostic layer for your distributed system
You probably don't need this library directly if you are:
- Building apps using existing transports like Bleu or ActorEdge
- Just defining distributed actors for your application
- ✅ Unified Envelope: Single
Envelopetype for bidirectional communication - ✅ Rich Metadata: Timestamps, versioning, and custom headers for tracing
- ✅ Actor Registry: Thread-safe actor instance tracking via
Mutex - ✅ Codable Codec: Complete
InvocationEncoder/Decoderimplementation - ✅ Generic Support: Full support for generic methods and generic actors
- ✅ Swift Runtime Integration: Uses
executeDistributedTargetfor method dispatch - ✅ Standard Errors: Serializable
RuntimeErrortypes - ✅ Symmetric Transport Protocol: Bidirectional communication where both peers can send invocations
- ✅ Error Propagation:
AsyncThrowingStreamfor transport-level error handling - ✅ Zero Dependencies: Pure Swift standard library
- ✅ Sendable-Safe: Full Swift 6 concurrency support
dependencies: [
.package(url: "https://github.com/1amageek/swift-actor-runtime", from: "0.3.0")
]import ActorRuntime
import Bleu // or any other transport
// Define your distributed actor
distributed actor TemperatureSensor {
typealias ActorSystem = BLEActorSystem
distributed func readTemperature() async -> Double {
return 22.5
}
}
// Server (Peripheral)
let system = BLEActorSystem.mock()
let sensor = TemperatureSensor(actorSystem: system)
try await system.startAdvertising(sensor)
// Client (Central)
let system = BLEActorSystem.mock()
let sensors = try await system.discover(TemperatureSensor.self)
let temp = try await sensors[0].readTemperature()import ActorRuntime
public final class MyTransport: DistributedTransport, Sendable {
public var messages: AsyncThrowingStream<Envelope, Error> {
AsyncThrowingStream { continuation in
// Listen for incoming messages on your transport
// Yield both invocations and responses
// Use continuation.finish(throwing:) for transport errors
}
}
public func start() async throws {
// Initialize connection (bind port, connect to peer, etc.)
}
public func send(_ envelope: Envelope) async throws {
// Serialize and send envelope over your protocol
switch envelope {
case .invocation(let inv):
// Send invocation request
case .response(let res):
// Send response
}
}
public func stop() async {
// Close connections and cleanup resources
}
}The transport protocol supports symmetric bidirectional communication - both peers can send invocations and responses, enabling P2P patterns and server-initiated calls.
┌──────────────────────────────────┐
│ Your Distributed Actors │
└────────────┬─────────────────────┘
│
┌────────────┴─────────────────────┐
│ Transport Implementation │
│ (Bleu, ActorEdge, Custom) │
└────────────┬─────────────────────┘
│
┌────────────┴─────────────────────┐
│ swift-actor-runtime │
│ ┌────────────────────────────┐ │
│ │ Envelope │ │
│ │ ├─ InvocationEnvelope │ │
│ │ └─ ResponseEnvelope │ │
│ │ ActorRegistry │ │
│ │ CodableInvocationEncoder │ │
│ │ CodableInvocationDecoder │ │
│ │ RuntimeError │ │
│ │ DistributedTransport │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
Unified message type for bidirectional communication:
// Wrap invocations and responses in a single type
let message: Envelope = .invocation(invocationEnvelope)
let reply: Envelope = .response(responseEnvelope)
// Pattern match to handle messages
for try await envelope in transport.messages {
switch envelope {
case .invocation(let inv):
// Handle incoming method call
case .response(let res):
// Handle response to a previous call
}
}Represents a distributed method call with metadata:
let envelope = InvocationEnvelope(
recipientID: "sensor-1",
senderID: "client-1", // Optional sender identifier
target: "readTemperature",
genericSubstitutions: [], // For generic methods
arguments: Data(),
metadata: .init(
headers: ["trace-id": "abc123"] // Custom headers for tracing
)
)Represents the result with metadata:
let response = ResponseEnvelope(
callID: envelope.callID,
result: .success(resultData),
metadata: .init(executionTime: 0.05) // Optional execution timing
)
// Convenient methods for adding metadata
let enriched = response
.withExecutionTime(0.05)
.withHeader("trace-id", value: "abc123")Tracks actor instances:
let registry = ActorRegistry()
registry.register(sensor, id: "sensor-1")
if let actor = registry.find(id: "sensor-1") {
// Execute method on actor
}
// Important: Cleanup when done to prevent memory leaks
registry.unregister(id: "sensor-1")
// Or clear all actors during shutdown
registry.clear()Memory Management: ActorRegistry maintains strong references. Always call unregister(id:) when actors are no longer needed to prevent memory leaks.
Standard error types:
throw RuntimeError.actorNotFound("sensor-1")
throw RuntimeError.methodNotFound("readTemperature")
throw RuntimeError.timeout(10.0)The messages stream uses AsyncThrowingStream to propagate transport-level errors:
// In your ActorSystem's message loop
do {
for try await envelope in transport.messages {
switch envelope {
case .invocation(let inv):
await handleInvocation(inv)
case .response(let res):
await handleResponse(res)
}
}
} catch {
// Handle transport errors (connection lost, deserialization failed, etc.)
print("Transport error: \(error)")
}Transport implementations can signal errors using the continuation:
public var messages: AsyncThrowingStream<Envelope, Error> {
AsyncThrowingStream { continuation in
// On successful message
continuation.yield(.invocation(envelope))
// On transport error
continuation.finish(throwing: RuntimeError.transportFailed("Connection lost"))
// On clean shutdown
continuation.finish()
}
}- Swift 6.2+
- iOS 18.0+
- macOS 15.0+
- watchOS 11.0+
- tvOS 18.0+
- visionOS 2.0+
The Codec system enables distributed method calls with Codable arguments:
// In your DistributedActorSystem implementation
func remoteCall<Act, Err, Res>(
on actor: Act,
target: RemoteCallTarget,
invocation: inout InvocationEncoder,
throwing: Err.Type,
returning: Res.Type
) async throws -> Res {
var encoder = invocation as! CodableInvocationEncoder
encoder.recordTarget(target)
let envelope = try encoder.makeInvocationEnvelope(
recipientID: actor.id.description
)
// Send envelope over your transport
try await transport.send(.invocation(envelope))
// Await response via messages stream (implementation handles callID matching)
let response = try await awaitResponse(callID: envelope.callID)
// Decode result
switch response.result {
case .success(let data):
return try JSONDecoder().decode(Res.self, from: data)
case .void:
return () as! Res
case .failure(let error):
throw error
}
}See Examples/InMemoryTransport.swift for a complete working implementation.
- Design Documentation - Detailed architecture and design decisions
- Codec System Design - InvocationEncoder/Decoder implementation details
- InMemoryTransport Example - Complete working transport implementation
MIT License