swift-actor-runtime

main

Transport-agnostic primitives for implementing Swift Distributed Actor systems.
1amageek/swift-actor-runtime

Swift Actor Runtime

Test Swift 6.2+ License: MIT

Transport-agnostic primitives for implementing Swift Distributed Actor systems.

Overview

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"

Features

  • Universal Envelopes: InvocationEnvelope and ResponseEnvelope for method calls
  • Actor Registry: Thread-safe actor instance tracking via Mutex
  • Codable Codec: Complete InvocationEncoder/Decoder implementation 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 executeDistributedTarget for method dispatch
  • Standard Errors: Serializable RuntimeError types
  • Transport Protocol: Common interface for all transport implementations
  • Zero Dependencies: Pure Swift standard library
  • Sendable-Safe: Full Swift 6 concurrency support

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/1amageek/swift-actor-runtime", from: "0.1.0")
]

Quick Start

Using a Transport (e.g., Bleu for BLE)

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()

Implementing a Transport

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
    }
}

Architecture

┌──────────────────────────────────┐
│    Your Distributed Actors       │
└────────────┬─────────────────────┘
             │
┌────────────┴─────────────────────┐
│    Transport Implementation      │
│   (Bleu, ActorEdge, Custom)      │
└────────────┬─────────────────────┘
             │
┌────────────┴─────────────────────┐
│    swift-actor-runtime           │
│  ┌────────────────────────────┐  │
│  │ InvocationEnvelope         │  │
│  │ ResponseEnvelope           │  │
│  │ ActorRegistry              │  │
│  │ CodableInvocationEncoder   │  │
│  │ CodableInvocationDecoder   │  │
│  │ RuntimeError               │  │
│  │ DistributedTransport       │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

Core Components

InvocationEnvelope

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.

ResponseEnvelope

Represents the result:

let response = ResponseEnvelope(
    callID: envelope.callID,
    result: .success(resultData)
)

ActorRegistry

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.

RuntimeError

Standard error types:

throw RuntimeError.actorNotFound("sensor-1")
throw RuntimeError.methodNotFound("readTemperature")
throw RuntimeError.timeout(10.0)

Platform Support

  • Swift 6.2+
  • iOS 18.0+
  • macOS 15.0+
  • watchOS 11.0+
  • tvOS 18.0+
  • visionOS 2.0+

Transports Using This Runtime

  • Bleu - BLE (Bluetooth Low Energy)
  • ActorEdge - gRPC
  • Your transport here!

Core Components

CodableInvocationEncoder / Decoder

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.

Documentation

License

MIT License

Author

@1amageek

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Nov 09 2025 02:35:51 GMT-1000 (Hawaii-Aleutian Standard Time)