Skip to content

Swift SDK

The Swift package is named HeddleActor. It provides Codable wire models, subject helpers, shallow schema validation, and a generic worker base.

Add the package

For a local checkout:

dependencies: [
    .package(path: "../../swift")
]

For a Git dependency:

dependencies: [
    .package(url: "https://github.com/getheddle/heddle-sdk.git", branch: "main")
]

Use the HeddleActor product. The package: identifier depends on which dependency form you used: the repo slug heddle-sdk for the Git URL form above, or the directory name swift for a local-path form (.package(path: "../../swift"), as used in examples/swift/echo-worker/Package.swift).

// Git-URL consumption (matches the dependency block above):
.product(name: "HeddleActor", package: "heddle-sdk")

// Local-path consumption (for in-repo development):
// .product(name: "HeddleActor", package: "swift")

Define payload and output types

import HeddleActor

struct EchoPayload: Codable, Sendable {
    var text: String
}

struct EchoOutput: Codable, Sendable {
    var text: String
    var length: Int
}

Payload types decode from TaskMessage.payload. Output types must encode to a JSON object; arrays and primitive outputs fail the worker contract because TaskResult.output is object-shaped.

Implement a worker

final class EchoWorker: HeddleWorker<EchoPayload, EchoOutput>, @unchecked Sendable {
    init() {
        super.init(workerType: "echo", tier: "local")
    }

    override func process(
        payload: EchoPayload,
        metadata: [String: JSONValue]
    ) async throws -> WorkerOutput<EchoOutput> {
        WorkerOutput(
            output: EchoOutput(
                text: payload.text.uppercased(),
                length: payload.text.count
            ),
            modelUsed: "swift-example"
        )
    }
}

Run with a transport

The core SDK defines the transport boundary:

public protocol HeddleTransport: Sendable {
    func publish(subject: String, payload: Data) async throws

    func subscribe(
        subject: String,
        queueGroup: String?
    ) async throws -> AsyncThrowingStream<HeddleMessage, Error>
}

The core package includes an in-memory implementation for local examples and tests:

let transport = InMemoryTransport()
try await EchoWorker().run(transport: transport)

A broker adapter can implement the same protocol without changing worker code:

try await EchoWorker().run(transport: natsTransport)

Use the shipped NATS adapter package for Heddle runtime interop:

import HeddleActorNATS

let transport = NatsTransport(url: URL(string: "nats://localhost:4222")!)
try await transport.connect()
try await EchoWorker().run(transport: transport)

The checked-in example uses InMemoryTransport so it can run without NATS while still exercising the transport loop:

swift run --package-path examples/swift/echo-worker EchoWorker

Swift concurrency notes

  • Payload and output types should be Sendable.
  • HeddleWorker is an open class and marked @unchecked Sendable because subclasses define their own state. Keep subclasses stateless between tasks.
  • Override reset() to clear temporary resources after each task.
  • Override malformedMessage(_:) to log malformed input without crashing the subscription loop.
  • InMemoryTransport is process-local. Use a shared broker transport for a native worker that needs to talk to a running Heddle or Workshop process.
  • HeddleActorNATS depends on the official nats-io/nats.swift package and stays separate from the core Swift package. The real NATS binding currently builds on macOS, matching the official client's published platform support.