Typed networking for Swift apps and services.
Comet turns API endpoints into Swift types. It ships with a URLSession-backed live HTTP client, middleware for production behavior, opt-in response caching, deterministic testing and contract transports, cassette recording and replay, OpenAPI request generation, request activity and trace streams, response streaming, transfer progress hooks, and resilient WebSocket sessions.
The latest published release is 0.4.5, the V3 foundation plus stale-while-revalidate caching and observability, proof bundle exports, the SwiftPM OpenAPI command plugin, expanded OpenAPI schema and request generation, YAML input, optional SQLiteData persistence, TCA playground coverage, cache and middleware hardening, server Swift live HTTP support, hardened OpenAPI output, LocalMockServer IPv6 handling, and tracked TCA cancellation.
| Surface | What It Provides |
|---|---|
Comet |
Typed HTTP requests, WebSocket sessions, serializers, middleware, retry, cache, deduplication, activity events, traces, streaming, and progress primitives |
CometTesting |
Mock transports, strict contracts, mock-server scenarios, cassette recording, replay transports, and mock WebSocket sessions |
CometOpenAPIGenerator |
JSON and YAML OpenAPI 3.x request generator core plus the comet-openapi-generate executable |
CometOpenAPIPlugin |
SwiftPM command plugin for package-root OpenAPI generation |
CometSQLiteData |
Optional SQLiteData tables, migrations, and storage helpers for Comet activity events and generated artifacts |
CometTCA |
Lightweight Composable Architecture helpers for request effects, tracked request effects, tracked cancellation, and request state |
CometPlayground |
iPhone-first verification app for HTTP, cache, contracts, replay, activity, and realtime flows |
- Swift 6.2
- iOS 18+
- macOS 15+
- visionOS 2+
- Linux builds for the core
Comettarget
The shipped live HTTP transport is URLSession-backed and imports FoundationNetworking where available, so server-side Swift can use URLSessionTransport for live HTTP requests without an additional Comet dependency. URLSessionWebSocketTransport uses URLSessionWebSocketTask and is available only on Apple platforms; server apps can provide a custom WebSocketTransport.
.package(url: "https://github.com/mrbagels/comet.git", from: "0.4.5")Import the target you need:
import Comet
import CometTesting
import CometOpenAPIGenerator
import CometSQLiteDataimport Comet
import HTTPTypes
struct User: Decodable, Sendable {
let id: Int
let name: String
}
struct GetUser: APIRequest {
let userID: Int
var path: Path { "users" / self.userID }
let method: HTTPMethod = .get
let responseSerializer: ResponseSerializer<User> = .json(User.self)
// API versioning is opt-in. Add it only when your server expects it.
var options: RequestOptions {
.init(apiVersion: "v1")
}
}
let client = HTTPClient.live(
configuration: ClientConfiguration(
baseURL: URL(string: "https://api.example.com")!,
middleware: [
BearerTokenMiddleware {
await authStore.accessToken
}
]
),
transport: URLSessionTransport()
)
let user = try await client.send(GetUser(userID: 42))For refresh and 401 replay, configure the auth coordinator once:
let auth = AuthenticationCoordinator.bearer(
token: { await authStore.accessToken },
refresh: { try await authStore.refreshAccessToken() }
)
let client = HTTPClient.live(
configuration: ClientConfiguration(
baseURL: URL(string: "https://api.example.com")!,
middleware: [AuthenticationMiddleware(coordinator: auth)]
),
transport: URLSessionTransport()
)import Comet
let client = HTTPClient.live(
configuration: ClientConfiguration(
baseURL: URL(string: "https://api.example.com")!,
middleware: [
TracePropagationMiddleware(),
RetryMiddleware(maxAttempts: 3),
LoggingMiddleware(logLevel: .verbose)
]
),
transport: URLSessionTransport()
)
Task {
for await event in client.activity {
print(event)
}
}Requests can carry metadata into logs and activity events. Retry behavior is conservative by default: RetryMiddleware retries safe methods such as GET automatically, while write requests need an idempotency key or an explicit retry policy.
var options: RequestOptions {
.init(
idempotencyKey: "create-user-\(draft.id)",
metadata: RequestMetadata(name: "CreateUser", tags: ["users"]),
statusValidation: .successOrNotModified
)
}Activity events also expose diagnostic helpers for UI and logging code:
for await event in client.activity {
print(event.kind)
print(event.diagnosticSummary)
}TracePropagationMiddleware writes the W3C traceparent header and completed RequestTrace values expose the propagated trace ID.
let context = TraceContext(
traceID: "4bf92f3577b34da6a3ce929d0e0e4736",
parentID: "00f067aa0ba902b7",
flags: "01"
)!
var options: RequestOptions {
.init(
metadata: RequestMetadata(
name: "GetUser",
operationID: "users.get",
traceContext: context
)
)
}
for await trace in client.traces {
print(trace.traceID as Any)
}let cache = MemoryHTTPCacheStore()
let client = HTTPClient.live(
configuration: ClientConfiguration(
baseURL: URL(string: "https://api.example.com")!,
middleware: [
CacheMiddleware(store: cache)
]
),
transport: URLSessionTransport()
)
var options: RequestOptions {
RequestOptions(cachePolicy: .returnCacheElseLoad)
}Use FileHTTPCacheStore when responses should survive process restarts:
let cache = FileHTTPCacheStore(
namespace: "api-v1",
maximumSizeBytes: 25 * 1024 * 1024
)returnCacheElseLoad serves fresh cached responses and revalidates stale entries
when ETag or Last-Modified validators are available. Use .revalidate to
force a conditional request, .staleWhileRevalidate to return stale cached data
immediately while a background refresh updates the store, .cacheOnly for
offline reads, .networkOnly to avoid reading or writing the cache, or
.reloadIgnoringCache to fetch and store a replacement.
Stale-while-revalidate foreground requests record the stale hit and refresh
scheduling decision on their completed RequestTrace. The background refresh
emits its own lifecycle activity and completed trace so cache updates and
refresh failures are observable without adding cache-specific event cases.
staleWhileRevalidate respects HTTP cache guardrails: entries marked
no-store, no-cache, must-revalidate, or shared-cache proxy-revalidate
fall back to synchronous revalidation instead of serving stale data.
For offline-tolerant reads, opt in to stale fallback when the network request fails:
RequestOptions(
cachePolicy: HTTPCachePolicy(
strategy: .returnCacheElseLoad,
allowsStaleIfError: true
)
)Cache decisions are included in completed traces:
for await trace in client.traces {
print(trace.cacheEvents.map(\.kind))
}QueryItem includes helpers for common optional, boolean, collection, joined, and date parameters.
var queryItems: [QueryItem] {
QueryItems {
QueryItem("search", searchTerm)
QueryItem.optional("limit", limit)
QueryItem.bool("includeArchived", includeArchived)
QueryItem.items("tag", values: tags)
QueryItem.joined("ids", values: selectedIDs)
QueryItem.date("createdAfter", cutoffDate, style: .iso8601)
}
}Requests can opt into decoding structured HTTP error bodies while preserving the raw NetworkError.http information.
struct APIError: Decodable, Sendable {
let code: String
let message: String
}
struct CreateUser: APIRequestWithErrorResponse {
typealias Response = User
typealias ErrorResponse = APIError
let path: Path = "users"
let method: HTTPMethod = .post
let responseSerializer: ResponseSerializer<User> = .json(User.self)
let errorResponseSerializer: ErrorResponseSerializer<APIError> = .json(APIError.self)
}
do {
let user = try await client.sendWithTypedErrors(CreateUser())
} catch let error as APIClientError<APIError> {
print(error.decodedErrorBody?.message ?? error.networkError.debugSummary)
}Prepared requests can produce shell-safe cURL output. Use HTTPClient.prepare(_:) when you want to inspect the exact transport-ready request before sending it. Multiline is the default for logs, while compact output is useful for copying into single-line fields. JSON request bodies can also be pretty-printed when multiline readability matters.
let preparedRequest = try client.prepare(CreateTodoRequest())
let curl = preparedRequest.curlCommand(style: .compact)
let readableCurl = preparedRequest.curlCommand(
options: CURLCommandOptions(
style: .multiline,
bodyFormatting: .prettyPrintedJSON
)
)Verbose request logging uses the same options:
LoggingMiddleware(
logLevel: .verbose,
curlCommandOptions: CURLCommandOptions(style: .compact)
)import Comet
let sockets = WebSocketClient.live(
transport: URLSessionWebSocketTransport()
)
let connection = try await sockets.connect(
WebSocketRequest(
url: URL(string: "wss://ws.postman-echo.com/raw")!,
timeout: .seconds(10)
)
)
try await connection.send(.text(#"{"kind":"echo","library":"Comet"}"#))
let reply = try await connection.receive()
try await connection.close(code: .normalClosure)For long-lived readers, use the message stream and stop iterating when the connection closes or the surrounding task is cancelled:
for try await message in connection.messages() {
// Handle .text or .data frames.
}For lifecycle events and bounded reconnect attempts, use a session:
let session = sockets.session(
for: WebSocketRequest(url: URL(string: "wss://ws.postman-echo.com/raw")!),
configuration: WebSocketSessionConfiguration(maximumReconnectAttempts: 3)
)
for try await event in session.events() {
// Handle .connected, .message, .disconnected, and .reconnecting.
}for try await line in client.lines(StreamEvents()) {
print(line)
}
for try await event in client.serverSentEvents(StreamEvents()) {
print(event.data)
}
let response = try await client.sendRaw(UploadAsset()) { progress in
print(progress.kind, progress.completedBytes, progress.totalBytes as Any)
}import Comet
import CometTesting
let recorder = RecordingTransport(base: URLSessionTransport())
let liveClient = HTTPClient.live(
configuration: .default(baseURL: URL(string: "https://api.example.com")!),
transport: recorder
)
_ = try await liveClient.send(GetUser(userID: 42))
let cassetteURL = URL(fileURLWithPath: "Tests/Fixtures/get-user-42.json")
let cassette = await recorder.cassette()
try cassette.write(to: cassetteURL)
let replay = try ReplayTransport(contentsOf: cassetteURL)
let replayClient = HTTPClient.live(
configuration: .default(baseURL: URL(string: "https://api.example.com")!),
transport: replay
)
let recordedUser = try await replayClient.send(GetUser(userID: 42))MockTransport is the fastest path for fully in-memory tests. RecordingTransport and ReplayTransport are for higher-fidelity fixture workflows when you want to capture live traffic once and replay it deterministically later.
Recorded cassettes can include URLs, headers, request bodies, response bodies, cookies, and authorization data. RecordingTransport redacts common sensitive headers by default and supports custom request and response body redaction. Review generated fixtures before committing them.
let recorder = RecordingTransport(
base: URLSessionTransport(),
redaction: RecordingRedaction(
redactRequestBody: { request in request.url.path.contains("sessions") },
redactResponseBody: { response in response.headers[.contentType] == "application/json" }
)
)RecordingRedaction is an alias for Comet's shared RedactionPolicy, so the same policy shape can be used for cassettes, logging, and cURL output.
Use ContractTransport when a test should fail on the first request shape drift:
let transport = ContractTransport(
expectations: [
ContractExpectation(
id: "get-user",
method: .get,
path: "/users/42",
headers: [
ContractHeaderExpectation(name: "accept", value: .exact("application/json"))
],
outcome: .response(
RawResponse(data: Data(#"{"id":42}"#.utf8), statusCode: 200)
)
)
]
)
let client = HTTPClient.live(
configuration: .default(baseURL: URL(string: "https://api.example.com")!),
transport: transport
)
_ = try await client.send(GetUser(userID: 42))
try await transport.verifyComplete()MockServer wraps the same contracts for demo and UI-test scenarios, and reports can be exported as JSON:
let report = await transport.report()
try report.write(to: URL(fileURLWithPath: "contract-report.json"))Recorded cassettes can become strict contracts:
let expectations = try cassette.contractExpectations()
let mockServer = MockServer(expectations: expectations)Use LocalMockServer when a demo, integration test, or UI test should hit the
same contracts through a real URLSessionTransport:
let localServer = try await LocalMockServer.start(expectations: expectations)
defer { localServer.stop() }
let client = HTTPClient.live(
configuration: .default(baseURL: localServer.baseURL),
transport: URLSessionTransport()
)Generate Comet request types from JSON or YAML OpenAPI 3.x documents:
swift run comet-openapi-generate --input openapi.json --output GeneratedAPI.swiftFrom a package checkout, use the SwiftPM command plugin when generated sources should be written relative to the package root:
swift package --allow-writing-to-package-directory comet-openapi-generate \
--input openapi.yaml \
--output Sources/API/GeneratedAPI.swiftThe generator supports path, query, header, and cookie parameters, reusable component parameters, component schema models, nested inline object structs, typed and free-form additionalProperties dictionaries, object models that combine named properties with additionalProperties, simple allOf object composition, oneOf and anyOf union enums, discriminator decoding for component unions, component alias $refs, local schema $refs, reusable request bodies and responses, JSON and +json media types, plain-text media types, form URL-encoded and multipart form-data request bodies, dictionary-backed form bodies, typed JSON and string serializers, operation metadata, security requirement tags, and typed error-response hooks. Unsupported features fail with diagnostics instead of silently generating partial behavior.
Reachability is modeled as a hint, not a correctness boundary:
let provider = StaticReachabilityHintProvider(
ReachabilitySnapshot(status: .reachable, isExpensive: false)
)
let snapshot = await provider.currentSnapshot()Use this to inform UI or retry choices, but keep transport errors as the source of truth.
CometSQLiteData is a separate product for apps that already use SQLiteData or want persisted diagnostics without pulling TCA into the graph.
Create a migrated database during app bootstrap:
import CometSQLiteData
import Dependencies
extension DependencyValues {
mutating func bootstrapDatabase() throws {
defaultDatabase = try CometSQLiteDataSchema.defaultDatabase()
}
}If you already own the SQLiteData migrator, call CometSQLiteDataSchema.registerMigrations(&migrator) instead.
Then record activity events or artifacts from the configured database:
@Dependency(\.defaultDatabase) var database
let store = CometSQLiteDataStore(database: database)
Task {
for await event in client.activity {
try await store.record(event: event)
}
}The target stores generic artifact rows, so cassette JSON, contract reports, generated schema snapshots, and app-specific diagnostics can share one persistence surface.
Examples/CometPlayground is an iPhone-first verification app generated with XcodeGen. It provides:
- a focused smoke test target:
CometPlaygroundTests - deterministic mock verification with
CometTesting.MockTransport - deterministic contract verification with
CometTesting.MockServer - deterministic socket verification with
CometTesting.MockWebSocketTransport - persisted activity history through the optional
CometSQLiteDataproduct - a reducer-backed request tab through the optional
CometTCAproduct - live transport checks through
URLSessionTransportandURLSessionWebSocketTransport - proof, structured activity, failure-gallery, request-inspector, and detail flows showing which APIs are exercised and what output to verify
The full walkthrough lives in Examples/CometPlayground/README.md.
The DocC catalog includes workflow articles for authenticated JSON requests, retries and activity, request tracing, streaming and progress, typed API errors, testing, cassettes, contracts, WebSockets, OpenAPI generation, and TCA integration.
Run the package tests:
swift test --disable-xctestGenerate the example Xcode project:
cd Examples/CometPlayground
xcodegen generateRun the example smoke tests:
SIMULATOR_ID="$(../../.github/scripts/select-ios-simulator.sh)"
xcodebuild test \
-project CometPlayground.xcodeproj \
-scheme CometPlaygroundApp \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-skipMacroValidation \
-skipPackagePluginValidation \
SWIFT_ENABLE_EXPLICIT_MODULES=NOGitHub Actions runs the Swift package suite, a Linux core-target build, secret scanning, public API break gating, and the iOS example smoke tests on every push to next and master.
Check for public API changes against the latest release:
.github/scripts/check-api-breaking-changes.sh v0.4.5Run a fresh external client smoke check:
.github/scripts/fresh-client-smoke.shnextis the default integration branch for upcoming work.masteris the stable release branch.- Short-lived
feat/,fix/,refactor/,docs/,chore/,spike/, andhotfix/branches should branch fromnext. - Normal work merges back into
next, and releases promote frommaster.
SVG brand assets live in Resources/Brand. The README uses the gradient icon directly from that folder, and the playground app bundles the same mark through its asset catalog and app icon set.
Sources/: package source targetsTests/: package test targetsExamples/CometPlayground/: XcodeGen-driven iOS demo appSources/CometOpenAPIGenerator/: JSON and YAML OpenAPI generator coreResources/Brand/: SVG logo and icon files for docs, README, and app assets.github/scripts/fresh-client-smoke.sh: external package integration smoke check.github/scripts/select-ios-simulator.sh: local and CI iPhone simulator selection.github/workflows/ci.yml: package and iOS smoke test automationdocs/ARCHITECTURE.md: architecture notesdocs/technical/SERVER_TRANSPORT_DECISION.md: server transport support decisiondocs/IMPLEMENTATION_PLAN.md: implementation plan and rollout notesdocs/PRODUCT_ROADMAP.md: product roadmap and feature planningdocs/RELEASE_PLAN_0_3.md: completed release plan from0.2.xto0.3.0
If you want to understand or modify the current core flows, start here:
- Sources/Comet/Core/HTTPClient.swift
- Sources/Comet/WebSockets/WebSocketTypes.swift
- Sources/CometTesting/MockWebSocketTransport.swift
- Sources/CometTesting/ContractTesting.swift
- Sources/CometOpenAPIGenerator/OpenAPIGenerator.swift
- Sources/CometTesting/RecordingTransport.swift
- Examples/CometPlayground/App/DemoCatalog.swift
- Examples/CometPlayground/App/HomeTab.swift
- Examples/CometPlayground/App/ActivityTab.swift
- Examples/CometPlayground/project.yml