Skip to content

Commit 3679296

Browse files
committed
Initial Commit
0 parents  commit 3679296

File tree

15 files changed

+413
-0
lines changed

15 files changed

+413
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

Package.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version:5.3
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "ThreadKit",
8+
platforms: [.iOS(.v12), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v12)],
9+
products: [
10+
// Products define the executables and libraries a package produces, and make them visible to other packages.
11+
.library(
12+
name: "ThreadKit",
13+
targets: ["ThreadKit"]),
14+
],
15+
dependencies: [
16+
// Dependencies declare other packages that this package depends on.
17+
// .package(url: /* package url */, from: "1.0.0"),
18+
],
19+
targets: [
20+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
21+
// Targets can depend on other targets in this package, and on products in packages this package depends on.
22+
.target(
23+
name: "ThreadKit",
24+
dependencies: []),
25+
.testTarget(
26+
name: "ThreadKitTests",
27+
dependencies: ["ThreadKit"]),
28+
]
29+
)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ThreadKit
2+
3+
A bunch of threading related helpers. Things like atomics, and stuff.

Sources/ThreadKit/Atomic.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
/// A property wrapper that ensures atomic access to a value. IE only one thing can write at a time.
4+
/// Multiple things can potentially read at the same time, just not during a write.
5+
/// By using `pthread` to do the locking, this safer then using a `DispatchQueue/barrier` as there isn't a chance
6+
/// of priority inversion.
7+
@propertyWrapper
8+
public final class Atomic<Value> {
9+
10+
private var value: Value
11+
private let lock: Lock = PThreadRWLock()
12+
13+
public init(wrappedValue value: Value) {
14+
self.value = value
15+
}
16+
17+
public var wrappedValue: Value {
18+
get {
19+
self.lock.readLock()
20+
defer { self.lock.unlock() }
21+
return self.value
22+
}
23+
set {
24+
self.lock.writeLock()
25+
self.value = newValue
26+
self.lock.unlock()
27+
}
28+
}
29+
30+
/// Provides an closure that will be called synchronously. This closure will be passed in the current value
31+
/// and it is free to modify it. Any modifications will be saved back to the original value.
32+
/// No other reads/writes will be allowed between when the closure is called and it returns.
33+
public func mutate(_ closure: (inout Value) -> Void) {
34+
self.lock.writeLock()
35+
closure(&value)
36+
self.lock.unlock()
37+
}
38+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
/// Delays calls a specified amount, and consolidates all calls that come in, during that delay. Only the last one will be fired.
4+
public class CallDelayAndConsolidator {
5+
6+
private let queue: DispatchQueue
7+
private var latestSetTime: CFTimeInterval?
8+
9+
/// The queue that all calls will be made one
10+
public init(queue: DispatchQueue) {
11+
self.queue = queue
12+
}
13+
14+
/// Queues up a call to be fired after some delay. If another call is submitted to this consolidator during that delay, the delay starts anew for that
15+
/// new call, and previous calls will be discarded.
16+
public func performCall(with delay: TimeInterval, call: @escaping () -> Void) {
17+
let setTime = CFAbsoluteTimeGetCurrent()
18+
latestSetTime = setTime
19+
queue.asyncAfter(deadline: .now() + delay) { [weak self] in
20+
guard let self = self, let latestSetTime = self.latestSetTime, latestSetTime == setTime else {
21+
return
22+
}
23+
self.latestSetTime = nil
24+
call()
25+
}
26+
}
27+
}

Sources/ThreadKit/Completion.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
/// Wraps a closure to be called, along with the queue on which to call it.
4+
///
5+
/// It is a common pattern that asynchronous methods take in a completion closure that is called when the work is done. However it is often not enforced
6+
/// what queue that closure gets called on. You can add a `queue` parameter to such methods that is used just for calling the completion handler
7+
/// and that is probably the most correct way. However that can have other issues. That queue property can shadow the type's private queue that you meant
8+
/// to do all the work on, and so you block the passed in `queue` while doing that work, instead of just when calling the completion closure (had this happen).
9+
/// You could also just always use some arbitrary queue to call the completion. But then you need to document which queue the closure is called on.
10+
/// This documentation often becomes incorrect after changes down the road, and now the completion is called on a different queue then documented (had this).
11+
/// This type enforces that the function's caller can choose which queue to use for the completion closure, it must be called on that closure, and no other
12+
/// work can easily be performed on that queue.
13+
public struct Completion<ClosureParam> {
14+
15+
/// Create the completion handler. If you wish to release the `queue` and `closure` just release your reference to this Completion struct.
16+
///
17+
/// - Parameters:
18+
/// - queue: The queue to use when calling `closure`
19+
/// - closure: The function to be called on `queue` at a later time.
20+
public init(queue: DispatchQueue, closure: @escaping (ClosureParam) -> Void) {
21+
self.queue = queue
22+
self.closure = closure
23+
}
24+
25+
/// Call this when you want to have the completion closure called.
26+
///
27+
/// - Parameter value: The value to pass to the completion closure.
28+
public func complete(value: ClosureParam) {
29+
self.queue.async {
30+
self.closure(value)
31+
}
32+
}
33+
34+
// MARK: - Private
35+
36+
private let queue: DispatchQueue
37+
private let closure: (ClosureParam) -> Void
38+
}

Sources/ThreadKit/Debouncer.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import UIKit
2+
3+
/// The purpose of this class is to "debounce" a 'call' that happens as a result of a rapid callback
4+
/// But needs to be rate limited for performance reasons.
5+
public class Debouncer {
6+
7+
/// The minimum time interval before calls will execute again
8+
private let bounceTime: CFTimeInterval
9+
10+
/// The recorded time of the last executed call
11+
private var timeOfLastCall: CFTimeInterval?
12+
13+
public init(bounceTime: CFTimeInterval) {
14+
self.bounceTime = bounceTime
15+
}
16+
17+
/// the provided closure will be executed on the current queue if the last executed call was longer ago
18+
/// than the provided bounce time.
19+
public func call(_ closure: () -> Void) {
20+
let currentTime = CACurrentMediaTime()
21+
if let timeOfLastCall = timeOfLastCall {
22+
if currentTime - timeOfLastCall > bounceTime {
23+
self.timeOfLastCall = currentTime
24+
closure()
25+
}
26+
} else {
27+
self.timeOfLastCall = currentTime
28+
closure()
29+
}
30+
}
31+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Foundation
2+
3+
/// A utility that performs automatic retries of a task that can fail with a exponential backoff delay between each retry attempt
4+
public enum DelayedRetry {
5+
private enum Constants {
6+
static let retryDelayForOneRetryRemaining: TimeInterval = 4.0
7+
}
8+
9+
/// What attempt number is this? Useful so if == 1 you know this is the first attempt and not actually a retry
10+
public typealias AttemptNumber = Int
11+
public typealias TaskResult = (_ success: Bool) -> Void
12+
public typealias Task = (AttemptNumber, @escaping TaskResult) -> Void
13+
public typealias RetryAttemptsFailed = (() -> Void)
14+
15+
/// Overload for testing where we can setup a faster delay so tests don't take 5+ seconds for this one test.
16+
static func performTask(on queue: DispatchQueue, attemptNumber: AttemptNumber, numberOfRetries: Int, delayCalculator: @escaping (Int) -> TimeInterval,
17+
finishedAllRetryAttempts: RetryAttemptsFailed?, task: @escaping Task) {
18+
queue.async {
19+
task(attemptNumber, { (success) in
20+
guard !success else {
21+
return
22+
}
23+
if numberOfRetries > 0 {
24+
let delay = delayCalculator(numberOfRetries)
25+
queue.asyncAfter(deadline: .now() + delay, execute: {
26+
performTask(on: queue, attemptNumber: attemptNumber + 1, numberOfRetries: numberOfRetries - 1, delayCalculator: delayCalculator,
27+
finishedAllRetryAttempts: finishedAllRetryAttempts, task: task)
28+
})
29+
} else if let finishedAllRetryAttempts = finishedAllRetryAttempts {
30+
queue.async(execute: finishedAllRetryAttempts)
31+
}
32+
})
33+
}
34+
}
35+
36+
/// Perform a task on a queue. If that task fails, this will retry running that task with an exponential growing delay before each retry
37+
/// attempt.
38+
///
39+
/// - Parameters:
40+
/// - queue: The queue that the `task` will be run on asynchronously
41+
/// - numberOfRetries: How many times will this `task` be rerun if you say it has failed
42+
/// - finishedAllRetryAttempts: Will be called on the `queue` only if we have retried the task `numberOfRetries` and all attempts have failed.
43+
/// - task: The task that is to be performed. Once you are finished your work inside the task, call the `TaskResult` parameter with a `true` if it
44+
/// was completed successfully (no retry needed), or `false` if the task should be retried. It is up to the caller to ensure that running the
45+
/// `task` multiple times won't cause issues. This closure is not retained beyond the minimum required to run/retry the task.
46+
public static func performTask(on queue: DispatchQueue, numberOfRetries: Int, finishedAllRetryAttempts: RetryAttemptsFailed?, task: @escaping Task) {
47+
performTask(on: queue, attemptNumber: 1, numberOfRetries: numberOfRetries, delayCalculator: delay(forNumberOfRemainingRetries:),
48+
finishedAllRetryAttempts: finishedAllRetryAttempts, task: task)
49+
}
50+
51+
/// Compute the delay for the next retry based on the number of retries remaining.
52+
///
53+
/// - Parameter retriesRemaining: The number of retires remaining before we stop retrying
54+
/// - Returns: How long to delay before starting the next retry. The delay is the longest if this is the last retry attempt, and is cut in half for each
55+
/// additional retry remaining. So 1 retry remain = 4 second delay, 2 retry remain = 2 second delay, 3 retry = 1 second delay and so on.
56+
static func delay(forNumberOfRemainingRetries retriesRemaining: Int) -> TimeInterval {
57+
if retriesRemaining <= 1 {
58+
return Constants.retryDelayForOneRetryRemaining
59+
} else {
60+
return delay(forNumberOfRemainingRetries: retriesRemaining - 1) / 2
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)