Skip to content

Commit

Permalink
0.46.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Mar 23, 2024
1 parent 2b67ea4 commit a000c6e
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 1 deletion.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Request execution](#request-execution)
- [`APIClientCaller`](#apiclientcaller)
- [Serializer](#serializer)
- [Some execution momdifiers](#some-execution-momdifiers)
- [Encoding and Decoding](#encoding-and-decoding)
- [ContentSerializer](#contentserializer)
- [Auth](#auth)
Expand Down Expand Up @@ -134,6 +135,10 @@ Custom callers can be created for different types of requests, such as WebSocket

The `.decodable` serializer uses the `.bodyDecoder` configuration, which can be customized with the `.bodyDecoder` modifier. The default `bodyDecoder` is `JSONDecoder()`.

#### Some execution momdifiers
- `.retry(limit:)` for retrying a request a specified number of times.
- `.throttle(interval:)` for throttling requests with a specified interval.

### Encoding and Decoding
There are several built-in configurations for encoding and decoding:
- `.bodyEncoder` for encoding a request body. Built-in encoders include `.json`, `.formURL` and `.multipartFormData`.
Expand Down Expand Up @@ -252,7 +257,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "0.45.0")
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "0.46.0")
],
targets: [
.target(
Expand Down
85 changes: 85 additions & 0 deletions Sources/SwiftAPIClient/Modifiers/ThrottleModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Foundation

public extension APIClient {

/// Throttles equal requests to the server.
/// - Parameters:
/// - interval: The interval to throttle requests by.
///
/// If the interval is nil, then `configs.throttleInterval` is used. This allows setting the default interval for all requests via `.configs(\.throttleInterval, value)`.
func throttle(interval: TimeInterval? = nil) -> APIClient {
throttle(interval: interval) { $0 }
}

/// Throttles requests to the server by request id.
/// - Parameters:
/// - interval: The interval for throttling requests.
/// - id: A closure to uniquely identify the request.
///
/// If the interval is nil, then `configs.throttleInterval` is used. This allows setting the default interval for all requests via `.configs(\.throttleInterval, value)`.
func throttle<ID: Hashable>(interval: TimeInterval? = nil, id: @escaping (URLRequest) -> ID) -> APIClient {
configs {
if let interval {
$0.throttleInterval = interval
}
}
.httpClientMiddleware(
RequestsThrottleMiddleware(cache: .shared, id: id)
)
}
}

public extension APIClient.Configs {

/// The interval to throttle requests by. Default is 10 seconds.
var throttleInterval: TimeInterval {
get { self[\.throttleInterval] ?? 10 }
set { self[\.throttleInterval] = newValue }
}
}

private final actor RequestsThrottlerCache {

static let shared = RequestsThrottlerCache()
private var responses: [AnyHashable: (Any, HTTPURLResponse)] = [:]

func response<T>(for request: AnyHashable) -> (T, HTTPURLResponse)? {
responses[request] as? (T, HTTPURLResponse)
}

func setResponse<T>(response: (T, HTTPURLResponse), for request: AnyHashable) {
responses[request] = response
}

func removeResponse(for request: AnyHashable) {
responses[request] = nil
}
}

private struct RequestsThrottleMiddleware<ID: Hashable>: HTTPClientMiddleware {

let cache: RequestsThrottlerCache
let id: (URLRequest) -> ID

func execute<T>(
request: URLRequest,
configs: APIClient.Configs,
next: (URLRequest, APIClient.Configs) async throws -> (T, HTTPURLResponse)
) async throws -> (T, HTTPURLResponse) {
let interval = configs.throttleInterval
guard interval > 0 else {
return try await next(request, configs)
}
let requestID = id(request)
if let response: (T, HTTPURLResponse) = await cache.response(for: requestID) {
return response
}
let (value, httpResponse) = try await next(request, configs)
await cache.setResponse(response: (value, httpResponse), for: requestID)
Task {
try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
await cache.removeResponse(for: requestID)
}
return (value, httpResponse)
}
}

0 comments on commit a000c6e

Please sign in to comment.