Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ contact_links:
url: https://github.com/pointfreeco/swift-clocks/discussions
about: Library Q&A, ideas, and more
- name: Documentation
url: https://pointfreeco.github.io/swift-clocks/main/documentation/clocks/
url: https://swiftpackageindex.com/pointfreeco/swift-clocks/main/documentation/clocks
about: Read the documentation
- name: Videos
url: https://www.pointfree.co/collections/concurrency/clocks
Expand Down
32 changes: 20 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,45 @@ on:
- main
pull_request:
branches:
- '*'
- "*"
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
macos:
name: macOS
runs-on: macos-14
macos-26:
runs-on: macos-26
strategy:
matrix:
config: ['debug', 'release']
xcode:
- "26.2"
- "26.4"
steps:
- uses: actions/checkout@v4
- name: Select Xcode 15.4
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- uses: actions/checkout@v5
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Print Swift version
run: swift --version
- name: Install Simulator Runtimes
if: matrix.xcode != '26.2'
uses: nick-fields/retry@v3
with:
timeout_minutes: 15
max_attempts: 3
command: xcodebuild -downloadAllPlatforms
- name: Run ${{ matrix.config }} tests
run: CONFIG=${{ matrix.config }} make test-all
# - name: Build for library evolution
# run: CONFIG=${{ matrix.config }} make build-for-library-evolution

linux:
name: Linux
runs-on: ubuntu-latest
strategy:
matrix:
config: ['debug', 'release']
config: ["debug", "release"]
swift:
- '5.10'
- "6.0"
container: swift:${{ matrix.swift }}
steps:
- uses: actions/checkout@v4
Expand Down
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ let package = Package(
],
swiftLanguageModes: [.v6]
)

for target in package.targets {
target.swiftSettings = target.swiftSettings ?? []
target.swiftSettings?.append(contentsOf: [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ImmutableWeakCaptures"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
])
}
49 changes: 0 additions & 49 deletions Package@swift-5.9.swift

This file was deleted.

7 changes: 3 additions & 4 deletions Sources/Clocks/ImmediateClock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,17 @@
}
}

public private(set) var now: Instant
public internal(set) var now: Instant
public private(set) var minimumResolution: Duration = .zero
private let lock = NSLock()
let lock = NSLock()

public init(now: Instant = .init()) {
self.now = now
}

public func sleep(until deadline: Instant, tolerance: Duration?) async throws {
public func sleep(until deadline: Instant, tolerance: Duration?) throws {
try Task.checkCancellation()
self.lock.sync { self.now = deadline }
await Task.megaYield()
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Clocks/Internal/Lock.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
public import Foundation

extension NSLock {
@inlinable
Expand Down
34 changes: 34 additions & 0 deletions Sources/Clocks/NonsendingClock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#if compiler(>=6.2)
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public protocol NonsendingClock<Duration>: Clock {
nonisolated(nonsending)
func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws
}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension NonsendingClock {
nonisolated(nonsending)
public func sleep(for duration: Duration, tolerance: Instant.Duration? = nil) async throws
{
try await sleep(until: now.advanced(by: duration), tolerance: tolerance)
}
}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension AnyClock: NonsendingClock {}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension ContinuousClock: NonsendingClock {}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension ImmediateClock: NonsendingClock {}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension SuspendingClock: NonsendingClock {}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension TestClock: NonsendingClock {}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
extension UnimplementedClock: NonsendingClock {}
#endif
2 changes: 1 addition & 1 deletion Sources/Clocks/SwiftUI.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#if (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) && canImport(SwiftUI)
import SwiftUI
public import SwiftUI

@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *)
extension EnvironmentValues {
Expand Down
6 changes: 3 additions & 3 deletions Sources/Clocks/TestClock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
[(
id: UUID,
deadline: Instant,
continuation: AsyncThrowingStream<Never, Error>.Continuation
continuation: AsyncThrowingStream<Never, any Error>.Continuation
)] = []

public init(now: Instant = .init()) {
Expand All @@ -104,12 +104,12 @@
try Task.checkCancellation()
let id = UUID()
do {
let stream: AsyncThrowingStream<Never, Error>? = self.lock.sync {
let stream: AsyncThrowingStream<Never, any Error>? = self.lock.sync {
guard deadline >= self.now
else {
return nil
}
return AsyncThrowingStream<Never, Error> { continuation in
return AsyncThrowingStream<Never, any Error> { continuation in
self.suspensions.append((id: id, deadline: deadline, continuation: continuation))
}
}
Expand Down
26 changes: 16 additions & 10 deletions Tests/ClocksTests/TestClocksTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Clocks
import ConcurrencyExtras
import XCTest

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
Expand Down Expand Up @@ -46,12 +47,13 @@ final class TestClockTests: XCTestCase, @unchecked Sendable {
XCTAssertEqual(ticks, 5)
}

func testRun() async {
func testRun() async throws {
let isFinished = ActorIsolated(false)
Task {
try await self.clock.sleep(until: self.clock.now.advanced(by: .seconds(1)))
await isFinished.setValue(true)
}
try await Task.sleep(for: .seconds(0.1))

var checkIsFinished = await isFinished.value
XCTAssertEqual(checkIsFinished, false)
Expand Down Expand Up @@ -95,21 +97,26 @@ final class TestClockTests: XCTestCase, @unchecked Sendable {
let timer = clock.timer(interval: .seconds(1))
.prefix(10)

let timerStarted = LockIsolated(false)
let task = Task {
timerStarted.withValue { $0 = true }
var ticks = 0
for await _ in timer {
ticks += 1
}
return ticks
}

while !timerStarted.withValue(\.self) { await Task.yield() }
await self.clock.run(timeout: .seconds(30))
let ticks = await task.value
XCTAssertEqual(ticks, 10)
}

func testRunWithReentrantUnitsOfWork() async throws {
let taskStarted = LockIsolated(false)
let task = Task {
taskStarted.withValue { $0 = true }
var count = 0
try await self.clock.sleep(until: self.clock.now.advanced(by: .seconds(1)))
count += 1
Expand All @@ -124,6 +131,7 @@ final class TestClockTests: XCTestCase, @unchecked Sendable {
return count
}

while !taskStarted.withValue(\.self) { await Task.yield() }
await self.clock.run(timeout: .seconds(30))
let ticks = try await task.value
XCTAssertEqual(ticks, 5)
Expand Down Expand Up @@ -193,39 +201,37 @@ final class TestClockTests: XCTestCase, @unchecked Sendable {
}

func testRunSorting() async throws {
let secondTaskStarted = LockIsolated(false)
let task = Task {
try await withThrowingTaskGroup(of: Int.self, returning: [Int].self) { group in
let firstTaskStarted = LockIsolated(false)
group.addTask {
firstTaskStarted.withValue { $0 = true }
try await self.clock.sleep(for: .seconds(2))
return 2
}
while !firstTaskStarted.withValue(\.self) { await Task.yield() }
group.addTask {
try await Task.sleep(nanoseconds: 500_000_000)
secondTaskStarted.withValue { $0 = true }
try await self.clock.sleep(for: .seconds(1))
return 1
}
return try await group.reduce(into: []) { $0.append($1) }
}
}

try await Task.sleep(nanoseconds: 1_000_000_000)
while !secondTaskStarted.withValue(\.self) { await Task.yield() }
await self.clock.run(timeout: .seconds(5))
let values = try await task.value

XCTAssertEqual(values, [1, 2])
}

func testSleepUntilExactlyNow() async throws {
let before = Date()
Task {
try await Task.sleep(for: .seconds(30))
try await Task.sleep(for: .seconds(1))
await clock.advance()
}
try await clock.sleep(until: clock.now)
XCTAssertEqual(
before.advanced(by: 30).timeIntervalSince1970,
Date().timeIntervalSince1970,
accuracy: 5
)
}
}