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"
- ✅ Universal Envelopes:
InvocationEnvelopeandResponseEnvelopefor method calls - ✅ Actor Registry: Thread-safe actor instance tracking via
Mutex - ✅ Codable Codec: Complete
InvocationEncoder/Decoderimplementation for Codable arguments - ✅ Generic Method Support: Full support for distributed methods with generic type parameters
- ✅ Generic Actor Support: Support for distributed actors with generic constraints
- ✅ Swift Runtime Integration: Uses
executeDistributedTargetfor method dispatch - ✅ Standard Errors: Serializable
RuntimeErrortypes - ✅ Transport Protocol: Common interface for all transport implementations
- ✅ 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.1.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 {
public func sendInvocation(_ envelope: InvocationEnvelope) async throws -> ResponseEnvelope {
// Your transport-specific code here
// 1. Serialize envelope
// 2. Send over your protocol (BLE, HTTP, etc.)
// 3. Await response
// 4. Return ResponseEnvelope
}
public var incomingInvocations: AsyncStream<InvocationEnvelope> {
AsyncStream { continuation in
// Listen for incoming RPCs on your transport
}
}
public func sendResponse(_ envelope: ResponseEnvelope) async throws {
// Send response back to caller
}
}┌──────────────────────────────────┐
│ Your Distributed Actors │
└────────────┬─────────────────────┘
│
┌────────────┴─────────────────────┐
│ Transport Implementation │
│ (Bleu, ActorEdge, Custom) │
└────────────┬─────────────────────┘
│
┌────────────┴─────────────────────┐
│ swift-actor-runtime │
│ ┌────────────────────────────┐ │
│ │ InvocationEnvelope │ │
│ │ ResponseEnvelope │ │
│ │ ActorRegistry │ │
│ │ CodableInvocationEncoder │ │
│ │ CodableInvocationDecoder │ │
│ │ RuntimeError │ │
│ │ DistributedTransport │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
Represents a distributed method call:
let envelope = InvocationEnvelope(
recipientID: "sensor-1",
target: "readTemperature",
genericSubstitutions: [], // Optional: for generic methods
arguments: Data()
)For generic methods, the envelope automatically captures type substitutions to ensure type-safe distributed calls.
Represents the result:
let response = ResponseEnvelope(
callID: envelope.callID,
result: .success(resultData)
)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)- 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...
let response = try await transport.sendInvocation(envelope)
// 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