| name | distributed-actors |
| description | Best practices for defining distributed actors with @Trebuchet macro, method requirements, stateful actors, annotations, and error handling. Use when users need help structuring actors, managing state persistence, or understanding actor deployment configuration. |
Defining Distributed Actors
Best practices for creating distributed actors with Trebuchet.
Overview
Distributed actors in Trebuchet follow Swift's distributed actor model with additional considerations for network communication and cloud deployment.
The @Trebuchet Macro
The simplest way to create a Trebuchet actor is with the @Trebuchet macro:
@Trebuchet
distributed actor GameRoom {
distributed func join(player: Player) -> RoomState
}
The @Trebuchet macro automatically:
- Adds
typealias ActorSystem = TrebuchetActorSystem
- Adds
TrebuchetActor protocol conformance (since v0.4.0)
This is equivalent to:
distributed actor GameRoom: TrebuchetActor {
typealias ActorSystem = TrebuchetActorSystem
init(actorSystem: TrebuchetActorSystem) {
self.actorSystem = actorSystem
}
distributed func join(player: Player) -> RoomState
}
TrebuchetActor Protocol
All Trebuchet distributed actors must conform to the TrebuchetActor protocol (automatically added by @Trebuchet):
public protocol TrebuchetActor: DistributedActor where ActorSystem == TrebuchetActorSystem {
init(actorSystem: TrebuchetActorSystem)
}
This protocol ensures:
- Consistent initialization interface across all actors
- CLI can automatically instantiate actors for dev server and deployment
- Supports custom initializers alongside the protocol requirement
Custom Initializers
You can add custom initializers while still conforming to TrebuchetActor:
@Trebuchet
distributed actor GameRoom {
let maxPlayers: Int
init(actorSystem: TrebuchetActorSystem) {
self.maxPlayers = 10
self.actorSystem = actorSystem
}
init(maxPlayers: Int, actorSystem: TrebuchetActorSystem) {
self.maxPlayers = maxPlayers
self.actorSystem = actorSystem
}
}
Method Requirements
All distributed methods must:
- Be marked with
distributed
- Have parameters that conform to
Codable
- Have a return type that conforms to
Codable (or be Void)
@Trebuchet
distributed actor UserService {
distributed func getUser(id: UUID) -> User
distributed func deleteUser(id: UUID)
distributed func updateUser(_ user: User) throws -> User
}
Stateful Actors
For actors that need persistent state in serverless environments, conform to StatefulActor:
import TrebuchetCloud
@Trebuchet
distributed actor ShoppingCart: StatefulActor {
typealias PersistentState = CartState
var persistentState = CartState()
distributed func addItem(_ item: Item) -> CartState {
persistentState.items.append(item)
return persistentState
}
func loadState(from store: any ActorStateStore) async throws {
if let state = try await store.load(for: id.id, as: CartState.self) {
persistentState = state
}
}
func saveState(to store: any ActorStateStore) async throws {
try await store.save(persistentState, for: id.id)
}
}
struct CartState: Codable, Sendable {
var items: [Item] = []
}
Actor Annotations
Use comments to configure actor deployment:
@Trebuchet
distributed actor HeavyProcessor {
distributed func process(data: Data) -> ProcessedResult
}
These annotations are read by the CLI during deployment and configure:
- memory: Memory allocation in MB (128-10240)
- timeout: Execution timeout in seconds (1-900)
- isolated: Run in dedicated Lambda function (vs shared)
Error Handling
Distributed methods can throw errors that are serialized across the network:
enum GameError: Error, Codable {
case roomFull
case invalidPlayer
case gameAlreadyStarted
}
@Trebuchet
distributed actor GameRoom {
distributed func join(player: Player) throws -> RoomState {
guard players.count < maxPlayers else {
throw GameError.roomFull
}
}
}
Error handling on the client:
do {
let state = try await room.join(player: me)
} catch GameError.roomFull {
print("Room is full")
} catch {
print("Unexpected error: \(error)")
}
Private State
Actors can have non-distributed state for local computation:
@Trebuchet
distributed actor GameRoom {
private var players: [Player] = []
private var startTime: Date?
distributed func join(player: Player) -> RoomState {
players.append(player)
return RoomState(players: players)
}
distributed func getPlayerCount() -> Int {
return players.count
}
}
Initialization
All actors must conform to TrebuchetActor protocol (automatically added by @Trebuchet), which requires an init(actorSystem:) initializer:
@Trebuchet
distributed actor GameRoom {
let maxPlayers: Int
init(actorSystem: TrebuchetActorSystem) {
self.maxPlayers = 10
self.actorSystem = actorSystem
}
init(maxPlayers: Int, actorSystem: TrebuchetActorSystem) {
self.maxPlayers = maxPlayers
self.actorSystem = actorSystem
}
}
let server = TrebuchetServer(transport: .webSocket(port: 8080))
let room = GameRoom(maxPlayers: 10, actorSystem: server.actorSystem)
await server.expose(room, as: "main-room")
Dynamic Actor Creation
Trebuchet supports creating actors on-demand when clients request them. This is useful for creating per-user actors or game rooms dynamically:
let server = TrebuchetServer(transport: .webSocket(port: 8080))
server.onActorRequest { (actorType: GameRoom.Type, id: String, system: TrebuchetActorSystem) in
let room = GameRoom(actorSystem: system)
return room
}
try await server.run()
When a client tries to resolve an actor that doesn't exist, the server will:
- Call the
onActorRequest callback
- Create the actor using the provided factory
- Automatically expose it with the requested ID
- Return it to the client
This enables lazy actor creation and reduces memory usage for infrequently used actors.
Actor Types
Different actor types have different characteristics:
Request-Response Actors
Simple synchronous operations:
@Trebuchet
distributed actor Calculator {
distributed func add(a: Int, b: Int) -> Int {
return a + b
}
}
Stateful Actors
Actors that maintain state between invocations:
@Trebuchet
distributed actor Counter: StatefulActor {
typealias PersistentState = Int
var persistentState = 0
distributed func increment() -> Int {
persistentState += 1
return persistentState
}
func loadState(from store: any ActorStateStore) async throws {
if let state = try await store.load(for: id.id, as: Int.self) {
persistentState = state
}
}
func saveState(to store: any ActorStateStore) async throws {
try await store.save(persistentState, for: id.id)
}
}
Streaming Actors
Actors that broadcast state changes:
@Trebuchet
distributed actor TodoList {
@StreamedState var state: State = State()
distributed func addTodo(title: String) -> TodoItem {
let todo = TodoItem(title: title)
state.todos.append(todo)
return todo
}
}
Best Practices
Keep Methods Small
distributed func join(player: Player) -> RoomState
distributed func joinAndStartGameAndNotifyAll(player: Player) -> ComplexResult
Use Value Types for Parameters
struct Player: Codable, Sendable {
let id: UUID
let name: String
}
class Player: Codable {
let id: UUID
let name: String
}
Validate Input
distributed func join(player: Player) throws -> RoomState {
guard !player.name.isEmpty else {
throw GameError.invalidPlayer
}
guard players.count < maxPlayers else {
throw GameError.roomFull
}
}
Document Public Methods
@Trebuchet
distributed actor GameRoom {
distributed func join(player: Player) throws -> RoomState {
}
}
See Also
- Getting started guide for basic actor setup
- Streaming guide for @StreamedState usage
- Cloud deployment guide for deploying actors to AWS Lambda
- SwiftUI integration guide for @RemoteActor usage