Skip to content

Commit 3df5506

Browse files
committed
Swift 6.2 as minimum toolchain version
1 parent 9d5e407 commit 3df5506

File tree

11 files changed

+1115
-197
lines changed

11 files changed

+1115
-197
lines changed

.spi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: 1
22
builder:
33
configs:
4-
- swift_version: '6.1'
4+
- swift_version: '6.2'
55
documentation_targets:
66
- Configuration
77
- ConfigurationTesting

Examples/hello-world-cli-example/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 6.1
1+
// swift-tools-version: 6.2
22

33
import PackageDescription
44

Package.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 6.1
1+
// swift-tools-version: 6.2
22

33
import PackageDescription
44
#if canImport(FoundationEssentials)
@@ -61,6 +61,7 @@ let enableAllTraitsExplicit = ProcessInfo.processInfo.environment["ENABLE_ALL_TR
6161

6262
let enableAllTraits = spiGenerateDocs || previewDocs || enableAllTraitsExplicit
6363
let addDoccPlugin = previewDocs || spiGenerateDocs
64+
let enableAllCIFlags = enableAllTraitsExplicit
6465

6566
traits.insert(
6667
.default(
@@ -77,6 +78,7 @@ let package = Package(
7778
traits: traits,
7879
dependencies: [
7980
.package(url: "https://github.com/apple/swift-system", from: "1.5.0"),
81+
.package(url: "https://github.com/apple/swift-collections", from: "1.3.0"),
8082
.package(url: "https://github.com/swift-server/swift-service-lifecycle", from: "2.7.0"),
8183
.package(url: "https://github.com/apple/swift-log", from: "1.6.3"),
8284
.package(url: "https://github.com/apple/swift-metrics", from: "2.7.0"),
@@ -92,6 +94,10 @@ let package = Package(
9294
name: "SystemPackage",
9395
package: "swift-system"
9496
),
97+
.product(
98+
name: "DequeModule",
99+
package: "swift-collections"
100+
),
95101
.product(
96102
name: "Logging",
97103
package: "swift-log",
@@ -179,8 +185,16 @@ for target in package.targets {
179185
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md
180186
settings.append(.enableUpcomingFeature("InternalImportsByDefault"))
181187

188+
// https://docs.swift.org/compiler/documentation/diagnostics/nonisolated-nonsending-by-default/
189+
settings.append(.enableUpcomingFeature("NonisolatedNonsendingByDefault"))
190+
182191
settings.append(.enableExperimentalFeature("AvailabilityMacro=Configuration 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"))
183192

193+
if enableAllCIFlags {
194+
// Ensure all public types are explicitly annotated as Sendable or not Sendable.
195+
settings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable"]))
196+
}
197+
184198
target.swiftSettings = settings
185199
}
186200

Sources/Configuration/MultiProvider.swift

Lines changed: 97 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -199,24 +199,27 @@ extension MultiProvider {
199199
_ body: (ConfigUpdatesAsyncSequence<MultiSnapshot, Never>) async throws -> Return
200200
) async throws -> Return {
201201
let providers = storage.providers
202-
let sources:
203-
[@Sendable (
204-
(ConfigUpdatesAsyncSequence<any ConfigSnapshotProtocol, Never>) async throws -> Void
205-
) async throws -> Void] = providers.map { $0.watchSnapshot }
206-
return try await combineLatestOneOrMore(
207-
elementType: (any ConfigSnapshotProtocol).self,
208-
sources: sources,
209-
updatesHandler: { updateArrays in
210-
try await body(
211-
ConfigUpdatesAsyncSequence(
212-
updateArrays
213-
.map { array in
214-
MultiSnapshot(snapshots: array)
215-
}
216-
)
202+
typealias UpdatesSequence = any (AsyncSequence<any ConfigSnapshotProtocol, Never> & Sendable)
203+
var updateSequences: [UpdatesSequence] = []
204+
updateSequences.reserveCapacity(providers.count)
205+
return try await withProvidersWatchingSnapshot(
206+
providers: ArraySlice(providers),
207+
updateSequences: &updateSequences,
208+
) { providerUpdateSequences in
209+
let updateArrays = combineLatestMany(
210+
elementType: (any ConfigSnapshotProtocol).self,
211+
failureType: Never.self,
212+
providerUpdateSequences
213+
)
214+
return try await body(
215+
ConfigUpdatesAsyncSequence(
216+
updateArrays
217+
.map { array in
218+
MultiSnapshot(snapshots: array)
219+
}
217220
)
218-
}
219-
)
221+
)
222+
}
220223
}
221224

222225
/// Asynchronously resolves a configuration value from nested providers.
@@ -290,43 +293,86 @@ extension MultiProvider {
290293
) async throws -> Return {
291294
let providers = storage.providers
292295
let providerNames = providers.map(\.providerName)
293-
let sources:
294-
[@Sendable (
295-
(
296-
ConfigUpdatesAsyncSequence<Result<LookupResult, any Error>, Never>
297-
) async throws -> Void
298-
) async throws -> Void] = providers.map { provider in
299-
{ handler in
300-
_ = try await provider.watchValue(forKey: key, type: type, updatesHandler: handler)
301-
}
302-
}
303-
return try await combineLatestOneOrMore(
304-
elementType: Result<LookupResult, any Error>.self,
305-
sources: sources,
306-
updatesHandler: { updateArrays in
307-
try await updatesHandler(
308-
ConfigUpdatesAsyncSequence(
309-
updateArrays
310-
.map { array in
311-
var results: [AccessEvent.ProviderResult] = []
312-
for (providerIndex, lookupResult) in array.enumerated() {
313-
let providerName = providerNames[providerIndex]
314-
results.append(.init(providerName: providerName, result: lookupResult))
315-
switch lookupResult {
316-
case .success(let value) where value.value == nil:
317-
// Got a success + nil from a nested provider, keep iterating.
318-
continue
319-
default:
320-
// Got a success + non-nil or an error from a nested provider, propagate that up.
321-
return (results, lookupResult.map { $0.value })
322-
}
296+
typealias UpdatesSequence = any (AsyncSequence<Result<LookupResult, any Error>, Never> & Sendable)
297+
var updateSequences: [UpdatesSequence] = []
298+
updateSequences.reserveCapacity(providers.count)
299+
return try await withProvidersWatchingValue(
300+
providers: ArraySlice(providers),
301+
updateSequences: &updateSequences,
302+
key: key,
303+
configType: type,
304+
) { providerUpdateSequences in
305+
let updateArrays = combineLatestMany(
306+
elementType: Result<LookupResult, any Error>.self,
307+
failureType: Never.self,
308+
providerUpdateSequences
309+
)
310+
return try await updatesHandler(
311+
ConfigUpdatesAsyncSequence(
312+
updateArrays
313+
.map { array in
314+
var results: [AccessEvent.ProviderResult] = []
315+
for (providerIndex, lookupResult) in array.enumerated() {
316+
let providerName = providerNames[providerIndex]
317+
results.append(.init(providerName: providerName, result: lookupResult))
318+
switch lookupResult {
319+
case .success(let value) where value.value == nil:
320+
// Got a success + nil from a nested provider, keep iterating.
321+
continue
322+
default:
323+
// Got a success + non-nil or an error from a nested provider, propagate that up.
324+
return (results, lookupResult.map { $0.value })
323325
}
324-
// If all nested results were success + nil, return the same.
325-
return (results, .success(nil))
326326
}
327-
)
327+
// If all nested results were success + nil, return the same.
328+
return (results, .success(nil))
329+
}
328330
)
329-
}
331+
)
332+
}
333+
}
334+
}
335+
336+
@available(Configuration 1.0, *)
337+
nonisolated(nonsending) private func withProvidersWatchingValue<ReturnInner>(
338+
providers: ArraySlice<any ConfigProvider>,
339+
updateSequences: inout [any (AsyncSequence<Result<LookupResult, any Error>, Never> & Sendable)],
340+
key: AbsoluteConfigKey,
341+
configType: ConfigType,
342+
body: ([any (AsyncSequence<Result<LookupResult, any Error>, Never> & Sendable)]) async throws -> ReturnInner
343+
) async throws -> ReturnInner {
344+
guard let provider = providers.first else {
345+
// Recursion termination, once we've collected all update sequences, execute the body.
346+
return try await body(updateSequences)
347+
}
348+
return try await provider.watchValue(forKey: key, type: configType) { updates in
349+
updateSequences.append(updates)
350+
return try await withProvidersWatchingValue(
351+
providers: providers.dropFirst(),
352+
updateSequences: &updateSequences,
353+
key: key,
354+
configType: configType,
355+
body: body
356+
)
357+
}
358+
}
359+
360+
@available(Configuration 1.0, *)
361+
nonisolated(nonsending) private func withProvidersWatchingSnapshot<ReturnInner>(
362+
providers: ArraySlice<any ConfigProvider>,
363+
updateSequences: inout [any (AsyncSequence<any ConfigSnapshotProtocol, Never> & Sendable)],
364+
body: ([any (AsyncSequence<any ConfigSnapshotProtocol, Never> & Sendable)]) async throws -> ReturnInner
365+
) async throws -> ReturnInner {
366+
guard let provider = providers.first else {
367+
// Recursion termination, once we've collected all update sequences, execute the body.
368+
return try await body(updateSequences)
369+
}
370+
return try await provider.watchSnapshot { updates in
371+
updateSequences.append(updates)
372+
return try await withProvidersWatchingSnapshot(
373+
providers: providers.dropFirst(),
374+
updateSequences: &updateSequences,
375+
body: body
330376
)
331377
}
332378
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftConfiguration open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftConfiguration project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftConfiguration project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
//===----------------------------------------------------------------------===//
15+
//
16+
// This source file is part of the Swift Async Algorithms open source project
17+
//
18+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
19+
// Licensed under Apache License v2.0 with Runtime Library Exception
20+
//
21+
// See https://swift.org/LICENSE.txt for license information
22+
//
23+
//===----------------------------------------------------------------------===//
24+
25+
// Vendored copy of https://github.com/apple/swift-async-algorithms/pull/360
26+
27+
/// Creates an asynchronous sequence that combines the latest values from many `AsyncSequence` types
28+
/// by emitting a tuple of the values. ``combineLatestMany(_:)`` only emits a value whenever any of the base `AsyncSequence`s
29+
/// emit a value (so long as each of the bases have emitted at least one value).
30+
///
31+
/// Finishes:
32+
/// ``combineLatestMany(_:)`` finishes when one of the bases finishes before emitting any value or
33+
/// when all bases finished.
34+
///
35+
/// Throws:
36+
/// ``combineLatestMany(_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed
37+
/// values will be dropped.
38+
@available(Configuration 1.0, *)
39+
internal func combineLatestMany<Element: Sendable, Failure: Error>(
40+
elementType: Element.Type = Element.self,
41+
failureType: Failure.Type = Failure.self,
42+
_ bases: [any (AsyncSequence<Element, Failure> & Sendable)]
43+
) -> some AsyncSequence<[Element], Failure> & Sendable {
44+
AsyncCombineLatestManySequence<Element, Failure>(bases)
45+
}
46+
47+
/// An `AsyncSequence` that combines the latest values produced from many asynchronous sequences into an asynchronous sequence of tuples.
48+
@available(Configuration 1.0, *)
49+
internal struct AsyncCombineLatestManySequence<Element: Sendable, Failure: Error>: AsyncSequence, Sendable {
50+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
51+
public typealias AsyncIterator = Iterator
52+
53+
typealias Base = AsyncSequence<Element, Failure> & Sendable
54+
let bases: [any Base]
55+
56+
init(_ bases: [any Base]) {
57+
self.bases = bases
58+
}
59+
60+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
61+
public func makeAsyncIterator() -> AsyncIterator {
62+
Iterator(
63+
storage: .init(self.bases)
64+
)
65+
}
66+
67+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
68+
public struct Iterator: AsyncIteratorProtocol {
69+
final class InternalClass {
70+
private let storage: CombineLatestManyStorage<Element, Failure>
71+
72+
fileprivate init(storage: CombineLatestManyStorage<Element, Failure>) {
73+
self.storage = storage
74+
}
75+
76+
deinit {
77+
self.storage.iteratorDeinitialized()
78+
}
79+
80+
func next() async throws(Failure) -> [Element]? {
81+
guard let element = try await self.storage.next() else {
82+
return nil
83+
}
84+
85+
// This force unwrap is safe since there must be a third element.
86+
return element
87+
}
88+
}
89+
90+
let internalClass: InternalClass
91+
92+
fileprivate init(storage: CombineLatestManyStorage<Element, Failure>) {
93+
self.internalClass = InternalClass(storage: storage)
94+
}
95+
96+
public mutating func next() async throws(Failure) -> [Element]? {
97+
try await self.internalClass.next()
98+
}
99+
}
100+
}
101+
102+
@available(*, unavailable)
103+
extension AsyncCombineLatestManySequence.Iterator: Sendable {}

0 commit comments

Comments
 (0)