atacan/usefulthings
A zero-dependency Swift utility library for async/await concurrency, rate limiting, retry logic, circuit breakers, and more. Built with Swift 6.1 and structured concurrency.
Rate Limiting
Eight actor-based, thread-safe rate limiter implementations behind a shared RateLimiter protocol. All are Sendable and safe for concurrent use.
Token Bucket
Classic token bucket algorithm. Tokens accumulate over time and allow controlled bursts.
let limiter = TokenBucketRateLimiter(capacity: 10, per: .seconds(1))
try await limiter.acquire()Leaky Bucket
Requests drain at a fixed rate, smoothing out traffic spikes.
let limiter = LeakyBucketRateLimiter(capacity: 10, per: .seconds(1))
try await limiter.acquire()Fixed Window
Counts requests within fixed time intervals. Simple and low-overhead.
let limiter = FixedWindowRateLimiter(limit: 100, window: .seconds(60))Sliding Window Log
Tracks individual request timestamps for precise limiting. Most accurate, O(n) memory.
let limiter = SlidingWindowLogRateLimiter(limit: 100, window: .seconds(60))Sliding Window Counter
Hybrid approach that approximates a sliding window with O(1) memory.
let limiter = SlidingWindowCounterRateLimiter(limit: 100, window: .seconds(60))Concurrency Limiter
Semaphore-style limiter that caps the number of concurrent operations.
let limiter = ConcurrencyLimiter(maxConcurrent: 5)
try await limiter.withPermit {
// at most 5 concurrent executions
}Adaptive Rate Limiter
Automatically adjusts its rate based on success/failure feedback from downstream services.
let limiter = AdaptiveRateLimiter(initialRate: 10.0, minRate: 1.0, maxRate: 100.0)
// call limiter.recordSuccess() or limiter.recordRateLimited() to adjustComposite Rate Limiter
Combines multiple limiters using Swift parameter packs. A request must pass all of them.
let composite = CompositeRateLimiter(tokenBucket, fixedWindow)
try await composite.acquire()Keyed Rate Limiter
Per-key limiting (e.g. per-user, per-IP). Creates limiters on demand.
let keyed = KeyedRateLimiter { TokenBucketRateLimiter(capacity: 10, per: .seconds(1)) }
try await keyed.acquire(forKey: userId)Retry with Exponential Backoff
Retry operations with configurable exponential backoff, jitter, typed throws, and cancellation support.
let result = try await withRetry(configuration: .default) {
try await fetchFromAPI()
}Retry Configuration Presets
| Preset | Attempts | Initial Delay | Max Delay | Backoff | Jitter | |---|---|---|---|---|---| | .default | 3 | 1s | 30s | 2.0x | 0.25 | | .aggressive | 5 | 0.5s | 60s | 2.0x | 0.25 | | .conservative | 10 | 2s | 120s | 3.0x | 0.5 |
Retry Predicates
Control which errors trigger retries with composable predicates.
try await withRetry(
predicate: .on(NetworkError.self).and(.except(AuthError.self))
) {
try await fetchFromAPI()
}Retry with Timeout
Abort the entire retry sequence if a deadline is exceeded.
try await withRetry(configuration: .aggressive, timeout: .seconds(30)) {
try await fetchFromAPI()
}Retry with Rate Limiter
Combine retries with rate limiting to avoid hammering a struggling service.
let limiter = TokenBucketRateLimiter(capacity: 5, per: .seconds(1))
let result = try await withRetry(rateLimiter: limiter) {
try await fetchFromAPI()
}Circuit Breaker
Prevent cascading failures by stopping calls to a failing dependency. Transitions through closed, open, and half-open states.
let breaker = CircuitBreaker(failureThreshold: 5, successThreshold: 2, timeout: .seconds(30))
let result = try await breaker.execute {
try await callExternalService()
}Combined Resilience
Apply rate limiting, circuit breaking, and retries in a single call.
try await withResilience(
rateLimiter: limiter,
circuitBreaker: breaker,
retryConfiguration: .default
) {
try await callExternalService()
}Polling
Poll an operation until a condition is met, with exponential backoff, jitter, timeout, and cancellation support.
let result = try await withPolling(
until: { $0.status == .completed },
operation: { try await checkJobStatus() }
)Polling Configuration
Configure attempts, delays, backoff, jitter, and an optional total timeout.
let config = PollingConfiguration(
maxAttempts: 20,
baseDelay: .milliseconds(500),
maxDelay: .seconds(10),
backoffMultiplier: 2.0,
jitterFactor: 0.5,
timeout: .seconds(60)
)
let result = try await withPolling(
configuration: config,
until: { $0.isReady },
operation: { try await pollServer() }
)Testable Polling with Clock Injection
Pass any Clock to control time in tests without real delays.
let result = try await withPolling(
configuration: .default,
clock: testClock,
until: { $0 == .done },
operation: { try await fetchStatus() }
)AsyncSequence Utilities
Prepend and Append to AsyncSequence
Wrap any AsyncSequence with prefix and/or suffix elements or sequences. Zero-cost abstractions using @inlinable.
let wrapped = stream.wrapped(prefix: headerElement, suffix: trailerElement)
for await element in wrapped {
// headerElement, then all stream elements, then trailerElement
}FileHandle as AsyncSequence
Read files asynchronously in chunks using for await.
let handle = FileHandle(forReadingAtPath: "/path/to/file")!
for try await chunk in handle {
// chunk is ArraySlice<UInt8>, default 64KB
}Streaming Base64 Encoding
Encode bytes, Data, or UTF-8 strings as an AsyncSequence of ArraySlice<UInt8> chunks. Initializers also accept synchronous and asynchronous sequences of byte chunks, Data, strings, individual bytes, or Character values.
let encoded = Base64Encoder(handle) // A FileHandle or another async byte-chunk source
for try await chunk in encoded {
// Write the Base64 ASCII bytes to your destination.
}
let text = Base64Encoder("Hello, world!")
let pieces = Base64Encoder(["Hello, ", "world!"], chunkSize: 4_096)The encoder reads on demand, keeps at most two pending bytes across input chunk boundaries, and adds = padding only at the end. It retains the current input chunk's iterator and a bounded output buffer without collecting the whole input. chunkSize limits output chunks (64 KiB by default) and must be a positive multiple of four. Upstream errors and task cancellation propagate to the consumer.
Streaming a Base64 Field in a JSON Request Body
Use the prefix/suffix wrapper to place streamed Base64 inside a nested JSON string:
let audioFile = try FileHandle(forReadingFrom: audioURL)
defer { try? audioFile.close() }
let prefix = ArraySlice(
#"{"config":{"encoding":"LINEAR16","sampleRateHertz":16000,"languageCode":"en-US"},"input":{"audio":{"content":""#.utf8
)
let suffix = ArraySlice(
#""},"enableTimestamps":true},"metadata":{"requestId":"streaming-example"}}"#.utf8
)
let body = Base64Encoder(audioFile)
.wrapped(prefix: prefix, suffix: suffix)
// Supply `body` to your HTTP client's streaming request-body API.
// Content-Type: application/jsonThe prefix opens the input.audio.content string and the suffix closes it. Standard Base64 bytes need no JSON escaping, so intermediate chunks go directly between those fragments without separators. The HTTP client must consume the sequence as a streaming body to keep the upload bounded in memory.
The executable example in StreamingJSONRequestBodyTests.swift uses a generated PCM audio file, verifies that sending the prefix does not read the file, and decodes the received JSON to recover the exact audio bytes. It covers empty audio, padding, and files spanning multiple input and output chunks.
Side Effect AsyncSequence
Tap into an AsyncSequence to perform side effects on each element without transforming it.
let tapped = SideEffectAsyncSequence(base: stream, process: { element in
logger.log("Received: \(element)")
}, onFinish: {
logger.log("Stream complete")
})Shell Command Execution (macOS)
Run shell commands and external processes from Swift.
let output = try runCommand("ls -la")
let info = try runExternalCommand(executablePath: "/usr/bin/git", arguments: ["status"])
let probe = try runFfprobe(ffprobeArguments: ["-show_format", "video.mp4"])Environment Variables
Read environment variables from the system or fall back to a .env file.
let apiKey = getEnvironmentVariable("API_KEY", from: envFileUrl)JSON Encoding
Pretty-print any Encodable value as formatted JSON.
let data = try prettyEncode(myStruct)Byte Conversions
Convert a Double to its raw [UInt8] byte representation.
let bytes = doubleToUInt8Array(3.14)Requirements
- Swift 6.1+
- macOS 14.0+ / iOS 17.0+ / watchOS 7.0+ / tvOS 14.0+ / visionOS 1.0+
- No external dependencies
Installation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/atacan/UsefulThings.git", branch: "main")
]Then add "UsefulThings" to the target dependencies:
.target(
name: "YourTarget",
dependencies: ["UsefulThings"]
)License
See LICENSE for details.
Package Metadata
Repository: atacan/usefulthings
Default branch: main
README: README.md