Files
SideStore/Pods/Nuke/Sources/Internal.swift

621 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The MIT License (MIT)
//
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
import Foundation
// MARK: - Lock
extension NSLock {
func sync<T>(_ closure: () -> T) -> T {
lock(); defer { unlock() }
return closure()
}
}
// MARK: - RateLimiter
/// Controls the rate at which the work is executed. Uses the classic [token
/// bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm.
///
/// The main use case for rate limiter is to support large (infinite) collections
/// of images by preventing trashing of underlying systems, primary URLSession.
///
/// The implementation supports quick bursts of requests which can be executed
/// without any delays when "the bucket is full". This is important to prevent
/// rate limiter from affecting "normal" requests flow.
internal final class RateLimiter {
private let bucket: TokenBucket
private let queue: DispatchQueue
private var pending = LinkedList<Task>() // fast append, fast remove first
private var isExecutingPendingTasks = false
private typealias Task = (CancellationToken, () -> Void)
/// Initializes the `RateLimiter` with the given configuration.
/// - parameter queue: Queue on which to execute pending tasks.
/// - parameter rate: Maximum number of requests per second. 80 by default.
/// - parameter burst: Maximum number of requests which can be executed without
/// any delays when "bucket is full". 25 by default.
init(queue: DispatchQueue, rate: Int = 80, burst: Int = 25) {
self.queue = queue
self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst))
}
func execute(token: CancellationToken, _ closure: @escaping () -> Void) {
let task = Task(token, closure)
if !pending.isEmpty || !_execute(task) {
pending.append(task)
_setNeedsExecutePendingTasks()
}
}
private func _execute(_ task: Task) -> Bool {
guard !task.0.isCancelling else {
return true // No need to execute
}
return bucket.execute(task.1)
}
private func _setNeedsExecutePendingTasks() {
guard !isExecutingPendingTasks else { return }
isExecutingPendingTasks = true
// Compute a delay such that by the time the closure is executed the
// bucket is refilled to a point that is able to execute at least one
// pending task. With a rate of 100 tasks we expect a refill every 10 ms.
let delay = Int(1.15 * (1000 / bucket.rate)) // 14 ms for rate 80 (default)
let bounds = max(100, min(5, delay)) // Make the delay is reasonable
queue.asyncAfter(deadline: .now() + .milliseconds(bounds), execute: _executePendingTasks)
}
private func _executePendingTasks() {
while let node = pending.first, _execute(node.value) {
pending.remove(node)
}
isExecutingPendingTasks = false
if !pending.isEmpty { // Not all pending items were executed
_setNeedsExecutePendingTasks()
}
}
private final class TokenBucket {
let rate: Double
private let burst: Double // maximum bucket size
private var bucket: Double
private var timestamp: TimeInterval // last refill timestamp
/// - parameter rate: Rate (tokens/second) at which bucket is refilled.
/// - parameter burst: Bucket size (maximum number of tokens).
init(rate: Double, burst: Double) {
self.rate = rate
self.burst = burst
self.bucket = burst
self.timestamp = CFAbsoluteTimeGetCurrent()
}
/// Returns `true` if the closure was executed, `false` if dropped.
func execute(_ closure: () -> Void) -> Bool {
refill()
guard bucket >= 1.0 else {
return false // bucket is empty
}
bucket -= 1.0
closure()
return true
}
private func refill() {
let now = CFAbsoluteTimeGetCurrent()
bucket += rate * max(0, now - timestamp) // rate * (time delta)
timestamp = now
if bucket > burst { // prevent bucket overflow
bucket = burst
}
}
}
}
// MARK: - Operation
internal final class Operation: Foundation.Operation {
private var _isExecuting = false
private var _isFinished = false
private var isFinishCalled = Atomic(false)
override var isExecuting: Bool {
set {
guard _isExecuting != newValue else {
fatalError("Invalid state, operation is already (not) executing")
}
willChangeValue(forKey: "isExecuting")
_isExecuting = newValue
didChangeValue(forKey: "isExecuting")
}
get {
return _isExecuting
}
}
override var isFinished: Bool {
set {
guard !_isFinished else {
fatalError("Invalid state, operation is already finished")
}
willChangeValue(forKey: "isFinished")
_isFinished = newValue
didChangeValue(forKey: "isFinished")
}
get {
return _isFinished
}
}
typealias Starter = (_ finish: @escaping () -> Void) -> Void
private let starter: Starter
init(starter: @escaping Starter) {
self.starter = starter
}
override func start() {
guard !isCancelled else {
isFinished = true
return
}
isExecuting = true
starter { [weak self] in
self?._finish()
}
}
private func _finish() {
// Make sure that we ignore if `finish` is called more than once.
if isFinishCalled.swap(to: true, ifEqual: false) {
isExecuting = false
isFinished = true
}
}
}
// MARK: - LinkedList
/// A doubly linked list.
internal final class LinkedList<Element> {
// first <-> node <-> ... <-> last
private(set) var first: Node?
private(set) var last: Node?
deinit {
removeAll()
}
var isEmpty: Bool {
return last == nil
}
/// Adds an element to the end of the list.
@discardableResult
func append(_ element: Element) -> Node {
let node = Node(value: element)
append(node)
return node
}
/// Adds a node to the end of the list.
func append(_ node: Node) {
if let last = last {
last.next = node
node.previous = last
self.last = node
} else {
last = node
first = node
}
}
func remove(_ node: Node) {
node.next?.previous = node.previous // node.previous is nil if node=first
node.previous?.next = node.next // node.next is nil if node=last
if node === last {
last = node.previous
}
if node === first {
first = node.next
}
node.next = nil
node.previous = nil
}
func removeAll() {
// avoid recursive Nodes deallocation
var node = first
while let next = node?.next {
node?.next = nil
next.previous = nil
node = next
}
last = nil
first = nil
}
final class Node {
let value: Element
fileprivate var next: Node?
fileprivate var previous: Node?
init(value: Element) {
self.value = value
}
}
}
// MARK: - CancellationTokenSource
/// Manages cancellation tokens and signals them when cancellation is requested.
///
/// All `CancellationTokenSource` methods are thread safe.
internal final class CancellationTokenSource {
/// Returns `true` if cancellation has been requested.
var isCancelling: Bool {
return lock.sync { observers == nil }
}
/// Creates a new token associated with the source.
var token: CancellationToken {
return CancellationToken(source: self)
}
private var lock = NSLock()
private var observers: [() -> Void]? = []
/// Initializes the `CancellationTokenSource` instance.
init() {}
fileprivate func register(_ closure: @escaping () -> Void) {
if !_register(closure) {
closure()
}
}
private func _register(_ closure: @escaping () -> Void) -> Bool {
lock.lock()
defer { lock.unlock() }
observers?.append(closure)
return observers != nil
}
/// Communicates a request for cancellation to the managed tokens.
func cancel() {
if let observers = _cancel() {
observers.forEach { $0() }
}
}
private func _cancel() -> [() -> Void]? {
lock.lock()
defer { lock.unlock() }
let observers = self.observers
self.observers = nil // transition to `isCancelling` state
return observers
}
}
/// Enables cooperative cancellation of operations.
///
/// You create a cancellation token by instantiating a `CancellationTokenSource`
/// object and calling its `token` property. You then pass the token to any
/// number of threads, tasks, or operations that should receive notice of
/// cancellation. When the owning object calls `cancel()`, the `isCancelling`
/// property on every copy of the cancellation token is set to `true`.
/// The registered objects can respond in whatever manner is appropriate.
///
/// All `CancellationToken` methods are thread safe.
internal struct CancellationToken {
fileprivate let source: CancellationTokenSource? // no-op when `nil`
/// Returns `true` if cancellation has been requested for this token.
/// Returns `false` if the source was deallocated.
var isCancelling: Bool {
return source?.isCancelling ?? false
}
/// Registers the closure that will be called when the token is canceled.
/// If this token is already cancelled, the closure will be run immediately
/// and synchronously.
func register(_ closure: @escaping () -> Void) {
source?.register(closure)
}
}
// MARK: - ResumableData
/// Resumable data support. For more info see:
/// - https://developer.apple.com/library/content/qa/qa1761/_index.html
internal struct ResumableData {
let data: Data
let validator: String // Either Last-Modified or ETag
init?(response: URLResponse, data: Data) {
// Check if "Accept-Ranges" is present and the response is valid.
guard !data.isEmpty,
let response = response as? HTTPURLResponse,
response.statusCode == 200 /* OK */ || response.statusCode == 206, /* Partial Content */
let acceptRanges = response.allHeaderFields["Accept-Ranges"] as? String,
acceptRanges.lowercased() == "bytes",
let validator = ResumableData._validator(from: response) else {
return nil
}
// NOTE: https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
// HTTP headers are case insensitive. To simplify your code, certain
// header field names are canonicalized into their standard form.
// For example, if the server sends a content-length header,
// it is automatically adjusted to be Content-Length.
self.data = data; self.validator = validator
}
private static func _validator(from response: HTTPURLResponse) -> String? {
if let entityTag = response.allHeaderFields["ETag"] as? String {
return entityTag // Prefer ETag
}
// There seems to be a bug with ETag where HTTPURLResponse would canonicalize
// it to Etag instead of ETag
// https://bugs.swift.org/browse/SR-2429
if let entityTag = response.allHeaderFields["Etag"] as? String {
return entityTag // Prefer ETag
}
if let lastModified = response.allHeaderFields["Last-Modified"] as? String {
return lastModified
}
return nil
}
func resume(request: inout URLRequest) {
var headers = request.allHTTPHeaderFields ?? [:]
// "bytes=1000-" means bytes from 1000 up to the end (inclusive)
headers["Range"] = "bytes=\(data.count)-"
headers["If-Range"] = validator
request.allHTTPHeaderFields = headers
}
// Check if the server decided to resume the response.
static func isResumedResponse(_ response: URLResponse) -> Bool {
// "206 Partial Content" (server accepted "If-Range")
return (response as? HTTPURLResponse)?.statusCode == 206
}
// MARK: Storing Resumable Data
/// Shared between multiple pipelines. Thread safe. In the future version we
/// might feature more customization options.
static var _cache = _Cache<String, ResumableData>(costLimit: 32 * 1024 * 1024, countLimit: 100) // internal only for testing purposes
static func removeResumableData(for request: URLRequest) -> ResumableData? {
guard let url = request.url?.absoluteString else { return nil }
return _cache.removeValue(forKey: url)
}
static func storeResumableData(_ data: ResumableData, for request: URLRequest) {
guard let url = request.url?.absoluteString else { return }
_cache.set(data, forKey: url, cost: data.data.count)
}
}
// MARK: - Printer
/// Helper type for printing nice debug descriptions.
internal struct Printer {
private(set) internal var _out = String()
private let timelineFormatter: DateFormatter
init(_ string: String = "") {
self._out = string
timelineFormatter = DateFormatter()
timelineFormatter.dateFormat = "HH:mm:ss.SSS"
}
func output(indent: Int = 0) -> String {
return _out.components(separatedBy: .newlines)
.map { $0.isEmpty ? "" : String(repeating: " ", count: indent) + $0 }
.joined(separator: "\n")
}
mutating func string(_ str: String) {
_out.append(str)
}
mutating func line(_ str: String) {
_out.append(str)
_out.append("\n")
}
mutating func value(_ key: String, _ value: CustomStringConvertible?) {
let val = value.map { String(describing: $0) }
line(key + " - " + (val ?? "nil"))
}
/// For producting nicely formatted timelines like this:
///
/// 11:45:52.737 - Data Loading Start Date
/// 11:45:52.739 - Data Loading End Date
/// nil - Decoding Start Date
mutating func timeline(_ key: String, _ date: Date?) {
let value = date.map { timelineFormatter.string(from: $0) }
self.value((value ?? "nil "), key) // Swtich key with value
}
mutating func timeline(_ key: String, _ start: Date?, _ end: Date?, isReversed: Bool = true) {
let duration = _duration(from: start, to: end)
let value = "\(_string(from: start)) \(_string(from: end)) (\(duration))"
if isReversed {
self.value(value.padding(toLength: 36, withPad: " ", startingAt: 0), key)
} else {
self.value(key, value)
}
}
mutating func section(title: String, _ closure: (inout Printer) -> Void) {
_out.append(contentsOf: title)
_out.append(" {\n")
var printer = Printer()
closure(&printer)
_out.append(printer.output(indent: 4))
_out.append("}\n")
}
// MARK: Formatters
private func _string(from date: Date?) -> String {
return date.map { timelineFormatter.string(from: $0) } ?? "nil"
}
private func _duration(from: Date?, to: Date?) -> String {
guard let from = from else { return "nil" }
guard let to = to else { return "unknown" }
return Printer.duration(to.timeIntervalSince(from)) ?? "nil"
}
static func duration(_ duration: TimeInterval?) -> String? {
guard let duration = duration else { return nil }
let m: Int = Int(duration) / 60
let s: Int = Int(duration) % 60
let ms: Int = Int(duration * 1000) % 1000
var output = String()
if m > 0 { output.append("\(m):") }
output.append(output.isEmpty ? "\(s)." : String(format: "%02d.", s))
output.append(String(format: "%03ds", ms))
return output
}
}
// MARK: - Misc
struct TaskMetrics {
var startDate: Date? = nil
var endDate: Date? = nil
static func started() -> TaskMetrics {
var metrics = TaskMetrics()
metrics.start()
return metrics
}
mutating func start() {
startDate = Date()
}
mutating func end() {
endDate = Date()
}
}
/// A simple observable property. Not thread safe.
final class Property<T> {
var value: T {
didSet {
for observer in observers {
observer(value)
}
}
}
init(value: T) {
self.value = value
}
private var observers = [(T) -> Void]()
// For our use-cases we can just ignore unsubscribing for now.
func observe(_ closure: @escaping (T) -> Void) {
observers.append(closure)
}
}
// MARK: - Atomic
/// A thread-safe value wrapper.
final class Atomic<T> {
private var _value: T
private let lock = NSLock()
init(_ value: T) {
self._value = value
}
var value: T {
get {
lock.lock()
let value = _value
lock.unlock()
return value
}
set {
lock.lock()
_value = newValue
lock.unlock()
}
}
}
extension Atomic where T: Equatable {
/// "Compare and Swap"
func swap(to newValue: T, ifEqual oldValue: T) -> Bool {
lock.lock()
defer { lock.unlock() }
guard _value == oldValue else {
return false
}
_value = newValue
return true
}
}
extension Atomic where T == Int {
/// Atomically increments the value and retruns a new incremented value.
func increment() -> Int {
lock.lock()
defer { lock.unlock() }
_value += 1
return _value
}
}
// MARK: - Misc
import CommonCrypto
extension String {
/// Calculates SHA1 from the given string and returns its hex representation.
///
/// ```swift
/// print("http://test.com".sha1)
/// // prints "50334ee0b51600df6397ce93ceed4728c37fee4e"
/// ```
var sha1: String? {
guard let input = self.data(using: .utf8) else { return nil }
#if swift(>=5.0)
let hash = input.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
CC_SHA1(bytes.baseAddress, CC_LONG(input.count), &hash)
return hash
}
#else
var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
input.withUnsafeBytes {
_ = CC_SHA1($0, CC_LONG(input.count), &hash)
}
#endif
return hash.map({ String(format: "%02x", $0) }).joined()
}
}