atacan/UsefulThings
a Swift utility library for rate limiting, retry with exponential backoff, circuit breakers, polling, and AsyncSequence utilities.
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
}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
Stars: 1
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md