Contents

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 adjust

Composite 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/json

The 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