Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ playground.xcworkspace
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm/

.build/

Expand Down Expand Up @@ -88,3 +88,6 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

# Ignore DS Store from Mac
.DS_Store
7 changes: 0 additions & 7 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

This file was deleted.

91 changes: 0 additions & 91 deletions .swiftpm/xcode/xcshareddata/xcschemes/AsyncTimeSequences.xcscheme

This file was deleted.

6 changes: 0 additions & 6 deletions FormatScript.sh

This file was deleted.

82 changes: 41 additions & 41 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
// swift-tools-version:5.5
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "AsyncTimeSequences",
platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AsyncTimeSequences",
targets: ["AsyncTimeSequences"]
),
.library(
name: "AsyncTimeSequencesSupport",
targets: ["AsyncTimeSequencesSupport"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "AsyncTimeSequences",
dependencies: [],
path: "Sources/AsyncTimeSequences"
),
.target(
name: "AsyncTimeSequencesSupport",
dependencies: [
"AsyncTimeSequences",
],
path: "Sources/AsyncTimeSequencesSupport"
),
.testTarget(
name: "AsyncTimeSequencesTests",
dependencies: [
"AsyncTimeSequences",
"AsyncTimeSequencesSupport"
],
path: "Tests"
),
]
name: "AsyncTimeSequences",
platforms: [.iOS(.v13), .macOS(.v13), .watchOS(.v6), .tvOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AsyncTimeSequences",
targets: ["AsyncTimeSequences"]
),
.library(
name: "AsyncTimeSequencesSupport",
targets: ["AsyncTimeSequencesSupport"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "AsyncTimeSequences",
dependencies: [],
path: "Sources/AsyncTimeSequences"
),
.target(
name: "AsyncTimeSequencesSupport",
dependencies: [
"AsyncTimeSequences"
],
path: "Sources/AsyncTimeSequencesSupport"
),
.testTarget(
name: "AsyncTimeSequencesTests",
dependencies: [
"AsyncTimeSequences",
"AsyncTimeSequencesSupport",
],
path: "Tests"
),
]
)
68 changes: 50 additions & 18 deletions Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public typealias AsyncSchedulerHandler = () async -> Void
public typealias AsyncSchedulerHandler = @Sendable () async -> Void

public protocol AsyncScheduler: Actor {
var now: TimeInterval { get }
Expand All @@ -23,6 +23,7 @@ public actor MainAsyncScheduler: AsyncScheduler {
lazy var idCounter: UInt = 0
lazy var completedElementIds = Set<UInt>()
lazy var cancelledElementIds = Set<UInt>()
private var isCompletingElement = false

public var now: TimeInterval {
Date().timeIntervalSince1970
Expand All @@ -34,8 +35,6 @@ public actor MainAsyncScheduler: AsyncScheduler {
/// - parameter handler: async closure to be executed when 'after' time elapses
///
/// - Returns: reference to a Task which supports cancellation
///
/// - Complexity: O(log n) where n is the number of elements currently scheduled
@discardableResult
public func schedule(
after: TimeInterval,
Expand All @@ -52,36 +51,69 @@ public actor MainAsyncScheduler: AsyncScheduler {

increaseCounterId()

return Task {
try? await Task.sleep(nanoseconds: UInt64(after * 1_000_000_000))
await complete(currentId: currentId, cancelled: Task.isCancelled)
}
return createScheduledExecutionTask(currentId: currentId, after: after)
}

/// Based on the timeIntervalSince1970 from Date, the smallest intervals will need
/// to complete before other elements' handlers can be executed. Due to the nature
/// of Tasks, there could be some situations where some tasks scheduled to finish
/// before others finish first. This could potentially have unwanted behaviors on
/// objects scheduling events. To address this matter, a minimum priority queue
/// is critical to always keep the first element that should be completed in the
/// top of the queue. Once its task completes, a Set will keep track of all
/// objects scheduling events.
///
/// - parameter currentId: integer variable denoting handler/task id
///
/// - Returns: reference to a Task which supports cancellation
private func createScheduledExecutionTask(
currentId: UInt,
after: TimeInterval
) -> Task<Void, Never> {
return Task {
try? await Task.sleep(for: .seconds(after))

completedElementIds.insert(currentId)
if Task.isCancelled {
cancelledElementIds.insert(currentId)
}

// Make sure that only one complete method is running at all times.
// The reason why this is important is because there is an inner await in a while loop which
// releases the execution of this actor and it cause race conditions if another scheduled
// task completes within the time this method is executing causing a weird state where two
// while loops might have erroneous values and destroy the serial execution intended from
// this method.
guard !isCompletingElement else { return }

// Block any other Tasks from calling complete from this point.
isCompletingElement = true

await complete(currentId: currentId)

// Allow any future callers of this method to call complete.
isCompletingElement = false
}
}

/// This method runs the completion handler for a given scheduled item matching the `currentId`.
///
/// A minimum priority queue is critical to always keep the first element that should be
/// completed in the top of the queue. Once its task completes, a Set will keep track of all
/// completed ID tasks that are yet to be executed. If the current top element of
/// the queue has already completed, its closure will execute. This will repeat
/// until all completed top elements of the queue are executed.
/// The obvious drawback of this handling, is that a small delay could be
/// introduced to some scheduled async-closures. Ideally, this would be in the
/// order of micro/nanoseconds depending of the system load.
///
/// - parameter currentId: integer variable denoting handler/task id
/// - parameter cancelled: boolean flag required to determine whether or not to execute the handler
/// This method will execute an inner loop resolving all available completed elements.
///
/// Note that his actor switches execution and during this `paused` time another scheduled task
/// can complete and call `complete`. It is important to have only one `complete` running at
/// all times (specifically, due the inner while loop).
///
/// This method should only be called from within `createScheduledExecutionTask`.
///
/// - Complexity: O(log n) where n is the number of elements currently scheduled
private func complete(currentId: UInt, cancelled: Bool) async {
completedElementIds.insert(currentId)
if cancelled {
cancelledElementIds.insert(currentId)
}

private func complete(currentId: UInt) async {
while let minElement = queue.peek, completedElementIds.contains(minElement.id) {
queue.removeFirst()
completedElementIds.remove(minElement.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct AsyncSchedulerHandlerElement {
extension AsyncSchedulerHandlerElement: Comparable {
static func < (lhs: AsyncSchedulerHandlerElement, rhs: AsyncSchedulerHandlerElement) -> Bool {
if lhs.time == rhs.time {
return lhs.id <= rhs.id
return lhs.id < rhs.id
}
return lhs.time < rhs.time
}
Expand Down
20 changes: 10 additions & 10 deletions Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Combine
import Foundation

public struct AsyncDebounceSequence<Base: AsyncSequence> {
public struct AsyncDebounceSequence<Base: AsyncSequence> where Base.Element: Sendable {
@usableFromInline
let base: Base

Expand All @@ -26,7 +26,7 @@ public struct AsyncDebounceSequence<Base: AsyncSequence> {
}
}

extension AsyncSequence {
extension AsyncSequence where Element: Sendable {
@inlinable
public __consuming func debounce(
for interval: TimeInterval,
Expand All @@ -36,7 +36,7 @@ extension AsyncSequence {
}
}

extension AsyncDebounceSequence: AsyncSequence {
extension AsyncDebounceSequence: AsyncSequence where Base.Element: Sendable {

public typealias Element = Base.Element
/// The type of iterator that produces elements of the sequence.
Expand Down Expand Up @@ -107,7 +107,7 @@ extension AsyncDebounceSequence: AsyncSequence {
}

@usableFromInline
struct Debounce {
struct Debounce: @unchecked Sendable {
private var baseIterator: Base.AsyncIterator
private let actor: DebounceActor

Expand Down Expand Up @@ -138,13 +138,13 @@ extension AsyncDebounceSequence: AsyncSequence {
@inlinable
public __consuming func makeAsyncIterator() -> AsyncStream<Base.Element>.Iterator {
return AsyncStream { (continuation: AsyncStream<Base.Element>.Continuation) in
var debounce = Debounce(
baseIterator: base.makeAsyncIterator(),
continuation: continuation,
interval: interval,
scheduler: scheduler
)
Task {
var debounce = Debounce(
baseIterator: base.makeAsyncIterator(),
continuation: continuation,
interval: interval,
scheduler: scheduler
)
await debounce.start()
}
}.makeAsyncIterator()
Expand Down
Loading