From 8d96935bf837308672c694a97456c8bbd5083b5f Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 24 Apr 2026 03:29:35 -0700 Subject: [PATCH 01/18] Dropped support for Swift 5 --- Package.swift | 14 ++++++------ Package@swift-6.0.swift | 47 ----------------------------------------- 2 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 Package@swift-6.0.swift diff --git a/Package.swift b/Package.swift index 1e7d6e3..100ef87 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. // @@ -13,6 +13,10 @@ import PackageDescription +let swiftSettings: [PackageDescription.SwiftSetting] = [ + .enableUpcomingFeature("ImmutableWeakCaptures"), +] + let package = Package( name: "CodableDatastore", platforms: [ @@ -38,16 +42,12 @@ let package = Package( "AsyncSequenceReader", "Bytes" ], - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), - ] + swiftSettings: swiftSettings ), .testTarget( name: "CodableDatastoreTests", dependencies: ["CodableDatastore"], - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), - ] + swiftSettings: swiftSettings ), ] ) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift deleted file mode 100644 index a3be699..0000000 --- a/Package@swift-6.0.swift +++ /dev/null @@ -1,47 +0,0 @@ -// swift-tools-version: 6.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -// -// Package@swift-6.0.swift -// https://github.com/mochidev/CodableDatastore -// -// Created by Dimitri Bouniol on 2024-06-19. -// Copyright © 2023-26 Mochi Development, Inc. All rights reserved. -// mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 -// - - -import PackageDescription - -let package = Package( - name: "CodableDatastore", - platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v6), - ], - products: [ - .library( - name: "CodableDatastore", - targets: ["CodableDatastore"] - ), - ], - dependencies: [ - .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.3.1")), - .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.3.0")), - ], - targets: [ - .target( - name: "CodableDatastore", - dependencies: [ - "AsyncSequenceReader", - "Bytes" - ] - ), - .testTarget( - name: "CodableDatastoreTests", - dependencies: ["CodableDatastore"] - ), - ] -) From 617ba0712451ee701dd4b6b7843a63acda0d5988 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 17 Jul 2026 05:10:35 -0700 Subject: [PATCH 02/18] Updated `Bytes` from 0.3.0 to 0.6.1 Update Package.swift --- Package.swift | 2 +- Tests/CodableDatastoreTests/DatastorePageEntryTests.swift | 8 ++++---- .../DiskPersistenceDatastoreIndexTests.swift | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 100ef87..7fdfb4d 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.3.1")), - .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.6.2")), ], targets: [ .target( diff --git a/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift b/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift index 51efa26..ba4b780 100644 --- a/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift +++ b/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift @@ -120,7 +120,7 @@ final class DatastorePageEntryTests: XCTestCase { ) ) { error in switch error { - case BytesError.invalidMemorySize(targetSize: 1, targetType: _, actualSize: 0): break + case BytesError.BufferSizeError.invalidBufferSize(targetSize: 1, targetType: "Byte", actualSize: 0): break default: XCTFail("Unknown error \(error)") } @@ -135,7 +135,7 @@ final class DatastorePageEntryTests: XCTestCase { ) ) { error in switch error { - case BytesError.checkedSequenceNotFound: break + case BytesError.SequenceCheckError.checkedSequenceNotFound: break default: XCTFail("Unknown error \(error)") } @@ -152,7 +152,7 @@ final class DatastorePageEntryTests: XCTestCase { ) ) { error in switch error { - case BytesError.invalidMemorySize(targetSize: 1, targetType: _, actualSize: 0): break + case BytesError.BufferSizeError.invalidBufferSize(targetSize: 1, targetType: "Byte", actualSize: 0): break default: XCTFail("Unknown error \(error)") } @@ -181,7 +181,7 @@ final class DatastorePageEntryTests: XCTestCase { ) ) { error in switch error { - case BytesError.invalidMemorySize(targetSize: 1, targetType: _, actualSize: 0): break + case BytesError.BufferSizeError.invalidBufferSize(targetSize: 1, targetType: "Byte", actualSize: 0): break default: XCTFail("Unknown error \(error)") } diff --git a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift index eca01d9..a129a34 100644 --- a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift +++ b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift @@ -11,6 +11,7 @@ @preconcurrency import Foundation #endif import XCTest +import Bytes @testable import CodableDatastore fileprivate struct SortError: Error, Equatable {} From 06d14a3f879838f51d38110dd48e4f700aa45339 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 17 Jul 2026 05:19:57 -0700 Subject: [PATCH 03/18] Updated `AsyncSequenceReader` from 0.3.1 to 0.5.0 --- Package.swift | 2 +- .../Datastore/DatastoreIndexManifest.swift | 4 ++-- .../Disk Persistence/Datastore/DatastorePage.swift | 14 +++++++------- .../Datastore/DatastorePageEntry.swift | 2 +- .../Datastore/DatastorePageEntryBlock.swift | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Package.swift b/Package.swift index 7fdfb4d..84b6921 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.3.1")), + .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.5.0")), .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.6.2")), ], targets: [ diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift index b927997..ffddbd4 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift @@ -7,9 +7,9 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation import AsyncSequenceReader import Bytes +import Foundation typealias DatastoreIndexManifestIdentifier = DatedIdentifier @@ -102,7 +102,7 @@ extension DatastoreIndexManifest { #endif } - init(sequence: AnyReadableSequence, id: ID) async throws { + init(sequence: AnyReadableSequence, id: ID) async throws { self.id = id var iterator = sequence.makeAsyncIterator() diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift index 72ac053..79bfa5b 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift @@ -7,9 +7,9 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation import AsyncSequenceReader import Bytes +import Foundation typealias DatastorePageIdentifier = DatedIdentifier.Datastore.Page> @@ -19,7 +19,7 @@ extension DiskPersistence.Datastore { let id: PersistenceDatastorePageID - var blocksReaderTask: Task>, Error>? + var blocksReaderTask: Task>, Error>? var isPersisted: Bool @@ -88,7 +88,7 @@ extension DiskPersistence.Datastore.Page { // MARK: - Persistence extension DiskPersistence.Datastore.Page { - private var readableSequence: AnyReadableSequence { + private var readableSequence: AnyReadableSequence { get throws { #if canImport(Darwin) if #available(macOS 12.0, iOS 15, watchOS 8, tvOS 15, *) { @@ -102,13 +102,13 @@ extension DiskPersistence.Datastore.Page { } } - private nonisolated func performRead(sequence: AnyReadableSequence) async throws -> MultiplexedAsyncSequence> { - var iterator = sequence.makeAsyncIterator() + private nonisolated func performRead(sequence: AnyReadableSequence) async throws -> MultiplexedAsyncSequence> { + var iterator = sequence.makeBufferedIterator() try await iterator.check(Self.header) /// Pages larger than 1 GB are unsupported. - let transformation = try await iterator.collect(max: Configuration.maximumPageSize) { sequence in + let transformation = await iterator.collect(max: Configuration.maximumPageSize) { sequence in sequence.iteratorMap { iterator in guard let block = try await iterator.next(DatastorePageEntryBlock.self) else { throw DiskPersistenceError.invalidPageFormat } @@ -123,7 +123,7 @@ extension DiskPersistence.Datastore.Page { } } - var blocks: MultiplexedAsyncSequence> { + var blocks: MultiplexedAsyncSequence> { get async throws { if let blocksReaderTask { return try await blocksReaderTask.value diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift index 1edbd6f..4ac0810 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift @@ -7,9 +7,9 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation import AsyncSequenceReader import Bytes +import Foundation struct DatastorePageEntry: Hashable { var headers: [Bytes] diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift index 5d9f90e..19d986f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift @@ -7,9 +7,9 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation import AsyncSequenceReader import Bytes +import Foundation /// A block of data that represents a portion of an entry on a page. @usableFromInline From 6bae3b29409747658944dae202ee8dce8ab2a217 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sat, 18 Jul 2026 05:45:48 -0700 Subject: [PATCH 04/18] Added intermediate `NonisolatedNonsendingByDefault` support --- .../Datastore/DatastorePage.swift | 14 +++++- .../Datastore/DatastorePageEntryBlock.swift | 46 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift index 79bfa5b..94bd186 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift @@ -110,9 +110,21 @@ extension DiskPersistence.Datastore.Page { /// Pages larger than 1 GB are unsupported. let transformation = await iterator.collect(max: Configuration.maximumPageSize) { sequence in sequence.iteratorMap { iterator in + #if canImport(Darwin) + if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { + guard let block = try await iterator.next(DatastorePageEntryBlock.self, isolation: #isolation) + else { throw DiskPersistenceError.invalidPageFormat } + return block + } else { + guard let block = try await iterator.next(DatastorePageEntryBlock.self) + else { throw DiskPersistenceError.invalidPageFormat } + return block + } + #else guard let block = try await iterator.next(DatastorePageEntryBlock.self) else { throw DiskPersistenceError.invalidPageFormat } return block + #endif } } @@ -218,7 +230,7 @@ actor MultiplexedAsyncSequence: AsyncSequence wh } extension RangeReplaceableCollection where Self: Sendable { - init(_ sequence: S) async throws where S.Element == Element { + init(_ sequence: sending S) async throws where S.Element == Element { self = try await sequence.reduce(into: Self.init()) { @Sendable partialResult, element in partialResult.append(element) } diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift index 19d986f..35a3503 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift @@ -35,7 +35,12 @@ enum DatastorePageEntryBlock: Hashable, Sendable { // MARK: - Decoding +#if canImport(Darwin) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension AsyncIteratorProtocol where Element == Byte { + #if swift(>=6.2) + @concurrent + #endif @usableFromInline mutating func next(_ type: DatastorePageEntryBlock.Type) async throws -> DatastorePageEntryBlock? { guard let blockType = try await nextIfPresent(utf8: String.self, count: 1) else { @@ -70,6 +75,47 @@ extension AsyncIteratorProtocol where Element == Byte { } } } +#endif // canImport(Darwin) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension AsyncIteratorProtocol where Element == Byte { + @usableFromInline + mutating func next( + _ type: DatastorePageEntryBlock.Type, + isolation actor: isolated (any Actor)? = #isolation + ) async throws -> DatastorePageEntryBlock? { + guard let blockType = try await nextIfPresent(utf8: String.self, count: 1) else { + return nil + } + + /// Fail early if the block type is not supported. + switch blockType { + case "<", "=", ">", "~": break + default: + throw DiskPersistenceError.invalidPageFormat + } + + /// Artificially limit ourselves to ~ 9 GB, though realistically our limit will be much, much lower. + guard let blockSizeBytes = try await collect(upToIncluding: "\n".utf8Bytes, throwsIfOver: 11) + else { throw DiskPersistenceError.invalidPageFormat } + + let decimalSizeString = String(utf8Bytes: blockSizeBytes.dropLast(1)) + guard let blockSize = Int(decimalSizeString), blockSize > 0 + else { throw DiskPersistenceError.invalidPageFormat } + + let payload = try await next(Bytes.self, count: blockSize) + + try await check(utf8: "\n") + + switch blockType { + case "<": return .tail(payload) + case "=": return .complete(payload) + case ">": return .head(payload) + case "~": return .slice(payload) + default: throw DiskPersistenceError.invalidPageFormat + } + } +} // MARK: - Encoding From d1678f4d359963ded532c57b8779dff9e0b7ffa5 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sat, 18 Jul 2026 05:55:05 -0700 Subject: [PATCH 05/18] Added support for `InferIsolatedConformances` and `MemberImportVisibility` --- Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Package.swift b/Package.swift index 84b6921..762ead5 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,8 @@ import PackageDescription let swiftSettings: [PackageDescription.SwiftSetting] = [ + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("InferIsolatedConformances"), .enableUpcomingFeature("ImmutableWeakCaptures"), ] From ffedd35fed0cb089b1b6daf1df57a0eb7ce4a07b Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sat, 18 Jul 2026 06:03:14 -0700 Subject: [PATCH 06/18] Added support for `InternalImportsByDefault` --- Package.swift | 1 + Sources/CodableDatastore/Datastore/Datastore.swift | 6 +----- .../CodableDatastore/Datastore/DatastoreDescriptor.swift | 2 +- Sources/CodableDatastore/Datastore/DatastoreError.swift | 2 +- Sources/CodableDatastore/Indexes/Indexable.swift | 2 +- Sources/CodableDatastore/Indexes/UUID+Comparable.swift | 2 +- .../Persistence/DatastoreInterfaceError.swift | 2 +- .../Persistence/DatastoreInterfaceProtocol.swift | 2 +- .../Datastore/DatastorePageEntryBlock.swift | 2 +- .../Persistence/Disk Persistence/DiskPersistence.swift | 6 +----- .../Persistence/Disk Persistence/DiskPersistenceError.swift | 2 +- 11 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Package.swift b/Package.swift index 762ead5..0bb4014 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,7 @@ import PackageDescription let swiftSettings: [PackageDescription.SwiftSetting] = [ + .enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("InferIsolatedConformances"), .enableUpcomingFeature("ImmutableWeakCaptures"), diff --git a/Sources/CodableDatastore/Datastore/Datastore.swift b/Sources/CodableDatastore/Datastore/Datastore.swift index 2f90a53..36203cb 100644 --- a/Sources/CodableDatastore/Datastore/Datastore.swift +++ b/Sources/CodableDatastore/Datastore/Datastore.swift @@ -7,11 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -#if canImport(Darwin) -import Foundation -#else -@preconcurrency import Foundation -#endif +public import Foundation /// A store for a homogenous collection of instances. public actor Datastore: Sendable { diff --git a/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift b/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift index a55163b..b31d05d 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// A description of a ``Datastore``'s requirements of a persistence. /// diff --git a/Sources/CodableDatastore/Datastore/DatastoreError.swift b/Sources/CodableDatastore/Datastore/DatastoreError.swift index 22bea19..56411e2 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreError.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreError.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// A ``Datastore``-specific error. public enum DatastoreError: LocalizedError { diff --git a/Sources/CodableDatastore/Indexes/Indexable.swift b/Sources/CodableDatastore/Indexes/Indexable.swift index 0bd623a..d96ba15 100644 --- a/Sources/CodableDatastore/Indexes/Indexable.swift +++ b/Sources/CodableDatastore/Indexes/Indexable.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// An alias representing the requirements for a property to be indexable, namely that they conform to both ``/Swift/Codable`` and ``/Swift/Comparable``. public typealias Indexable = Comparable & Hashable & Codable & Sendable diff --git a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift index 5a34e42..49d36fd 100644 --- a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift +++ b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// Make UUIDs comparable on platforms that shipped without it, so that they can be used transparently as an index. #if !canImport(FoundationEssentials) diff --git a/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift b/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift index 0295a26..13bb8c9 100644 --- a/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift +++ b/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// An error that may be returned from ``DatastoreInterfaceProtocol`` methods. public enum DatastoreInterfaceError: LocalizedError { diff --git a/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift b/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift index 6f1b35d..da24530 100644 --- a/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift +++ b/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// A interface a ``Datastore`` uses to communicate with a ``Persistence``. /// diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift index 35a3503..a8d0f5c 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift @@ -8,7 +8,7 @@ // import AsyncSequenceReader -import Bytes +public import Bytes import Foundation /// A block of data that represents a portion of an entry on a page. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift index 78a3b26..3eaa568 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift @@ -7,11 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -#if canImport(Darwin) -import Foundation -#else -@preconcurrency import Foundation -#endif +public import Foundation public actor DiskPersistence: Persistence { /// The location of this persistence. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift index 13c931c..37e70c0 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift @@ -7,7 +7,7 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // -import Foundation +public import Foundation /// A ``DiskPersistence``-specific error. public enum DiskPersistenceError: LocalizedError, Equatable { From 9be6ca2b726ceace68bf85df78d687558b8f8e8f Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sat, 18 Jul 2026 06:05:04 -0700 Subject: [PATCH 07/18] Removed Swift 5 hacks to retroactively conform `UUID` to `Comparable` --- .../Indexes/UUID+Comparable.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift index 49d36fd..3f31752 100644 --- a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift +++ b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift @@ -7,12 +7,10 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // +#if !canImport(FoundationEssentials) public import Foundation /// Make UUIDs comparable on platforms that shipped without it, so that they can be used transparently as an index. -#if !canImport(FoundationEssentials) -#if swift(<5.9) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Windows) -#if compiler(>=6) extension UUID: @retroactive Comparable { @inlinable @_disfavoredOverload @@ -20,17 +18,6 @@ extension UUID: @retroactive Comparable { lhs.uuid < rhs.uuid } } -#else -extension UUID: Comparable { - @inlinable - @_disfavoredOverload - public static func < (lhs: UUID, rhs: UUID) -> Bool { - lhs.uuid < rhs.uuid - } -} -#endif -#endif -#endif /// Make UUIDs comparable, so that they can be used transparently as an index. /// @@ -57,3 +44,4 @@ public func < (lhs: uuid_t, rhs: uuid_t) -> Bool { return result < 0 } +#endif From 22ada52b9112df4efaaf95b86e2a8608b49232e1 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 19 Jul 2026 05:46:44 -0700 Subject: [PATCH 08/18] Added support for `ExistentialAny` --- Package.swift | 1 + Sources/CodableDatastore/Datastore/Datastore.swift | 4 ++-- .../CodableDatastore/Datastore/DatastoreKey.swift | 4 ++-- Sources/CodableDatastore/Indexes/IndexName.swift | 4 ++-- Sources/CodableDatastore/Indexes/IndexType.swift | 4 ++-- .../AsyncThrowingBackpressureStream.swift | 8 ++++---- .../Datastore/DatastoreIndex.swift | 4 ++-- .../Disk Persistence/Datastore/DatastorePage.swift | 6 +++--- .../Datastore/PersistenceDatastore.swift | 2 +- .../Disk Persistence/DiskPersistence.swift | 12 ++++++------ .../Disk Persistence/Snapshot/Snapshot.swift | 6 +++--- .../Disk Persistence/Transaction/Transaction.swift | 14 +++++++------- .../Disk Persistence/TypedIdentifier.swift | 4 ++-- .../Memory Persistence/MemoryPersistence.swift | 2 +- .../CodableDatastore/Persistence/Persistence.swift | 2 +- 15 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Package.swift b/Package.swift index 0bb4014..fbbefc2 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,7 @@ import PackageDescription let swiftSettings: [PackageDescription.SwiftSetting] = [ + .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("InferIsolatedConformances"), diff --git a/Sources/CodableDatastore/Datastore/Datastore.swift b/Sources/CodableDatastore/Datastore/Datastore.swift index 36203cb..a16410b 100644 --- a/Sources/CodableDatastore/Datastore/Datastore.swift +++ b/Sources/CodableDatastore/Datastore/Datastore.swift @@ -173,7 +173,7 @@ extension Datastore { } } - func registerAndMigrate(with transaction: DatastoreInterfaceProtocol) async throws -> Progress { + func registerAndMigrate(with transaction: any DatastoreInterfaceProtocol) async throws -> Progress { let persistedDescriptor = try await transaction.register(datastore: self) /// Only operate on read-write datastores beyond this point. @@ -1676,6 +1676,6 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance private enum TaskStatus { case waiting - case inProgress(Task) + case inProgress(Task) case complete(Value) } diff --git a/Sources/CodableDatastore/Datastore/DatastoreKey.swift b/Sources/CodableDatastore/Datastore/DatastoreKey.swift index 0cf93e9..e01e4f8 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreKey.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreKey.swift @@ -30,14 +30,14 @@ extension DatastoreKey: ExpressibleByStringLiteral { } extension DatastoreKey: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(String.self)) } } extension DatastoreKey: Encodable { - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) } diff --git a/Sources/CodableDatastore/Indexes/IndexName.swift b/Sources/CodableDatastore/Indexes/IndexName.swift index 6508440..fcd3b32 100644 --- a/Sources/CodableDatastore/Indexes/IndexName.swift +++ b/Sources/CodableDatastore/Indexes/IndexName.swift @@ -31,14 +31,14 @@ extension IndexName: ExpressibleByStringLiteral { } extension IndexName: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(String.self)) } } extension IndexName: Encodable { - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) } diff --git a/Sources/CodableDatastore/Indexes/IndexType.swift b/Sources/CodableDatastore/Indexes/IndexType.swift index 71b7fce..1ad06ee 100644 --- a/Sources/CodableDatastore/Indexes/IndexType.swift +++ b/Sources/CodableDatastore/Indexes/IndexType.swift @@ -35,14 +35,14 @@ extension IndexType: ExpressibleByStringLiteral { } extension IndexType: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(String.self)) } } extension IndexType: Encodable { - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) } diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift index 2bcde04..e584b53 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift @@ -18,11 +18,11 @@ import Foundation /// The reading task may be cancelled at any time, immediately ending the loop, and propagaing the cancellation to the writing child task, stopping any more values from being provided to the stream. struct AsyncThrowingBackpressureStream: Sendable { fileprivate actor StateMachine { - var pendingWriteEvents: [(CheckedContinuation, Result)] = [] - var pendingReadContinuation: CheckedContinuation? + var pendingWriteEvents: [(CheckedContinuation, Result)] = [] + var pendingReadContinuation: CheckedContinuation? var wasCancelled = false - func provide(_ result: Result, in continuation: CheckedContinuation) { + func provide(_ result: Result, in continuation: CheckedContinuation) { /// If reads were cancelled, propagate the cancellation to the provider without saving the result. guard !wasCancelled else { continuation.resume(throwing: CancellationError()) @@ -128,7 +128,7 @@ struct AsyncThrowingBackpressureStream: Sendable { } } - fileprivate func finish(throwing error: Error? = nil) async throws { + fileprivate func finish(throwing error: (any Error)? = nil) async throws { try await withCheckedThrowingContinuation { continuation in guard let stateMachine else { continuation.resume(throwing: CancellationError()) return diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift index e961503..4dd5bb0 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift @@ -23,9 +23,9 @@ extension DiskPersistence.Datastore { let id: PersistenceDatastoreIndexID var _manifest: DatastoreIndexManifest? - var manifestTask: Task? + var manifestTask: Task? - var cachedOrderedPages: Task<[LazyTask?], Error>? + var cachedOrderedPages: Task<[LazyTask?], any Error>? var isPersisted: Bool diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift index 94bd186..f1bf280 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift @@ -19,7 +19,7 @@ extension DiskPersistence.Datastore { let id: PersistenceDatastorePageID - var blocksReaderTask: Task>, Error>? + var blocksReaderTask: Task>, any Error>? var isPersisted: Bool @@ -174,7 +174,7 @@ extension DiskPersistence.Datastore.Page { actor MultiplexedAsyncSequence: AsyncSequence where Base.Element: Sendable, Base.AsyncIterator: Sendable, Base.AsyncIterator.Element: Sendable { typealias Element = Base.Element - private var cachedEntries: [Task] = [] + private var cachedEntries: [Task] = [] private var baseIterator: Base.AsyncIterator? struct AsyncIterator: AsyncIteratorProtocol & Sendable { @@ -196,7 +196,7 @@ actor MultiplexedAsyncSequence: AsyncSequence wh precondition(index == cachedEntries.count, "\(index) is out of bounds.") - let lastTask: Task? = cachedEntries.last + let lastTask: Task? = cachedEntries.last let newTask = Task { /// Make sure previous iteration finished before sourcing the next one. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift index 8d96cbe..45f22b9 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift @@ -27,7 +27,7 @@ extension DiskPersistence { var cachedRootObject: DatastoreRootManifest? - var lastUpdateDescriptorTask: Task? + var lastUpdateDescriptorTask: Task? /// The root objects that are being tracked in memory. var trackedRootObjects: [RootObject.ID : WeakValue] = [:] diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift index 3eaa568..58ae78f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift @@ -17,7 +17,7 @@ public actor DiskPersistence: Persistence { var cachedStoreInfo: StoreInfo? /// A pointer to the last store info updater, so updates can be serialized after the last request - var lastUpdateStoreInfoTask: Task? + var lastUpdateStoreInfoTask: Task? /// The loaded Snapshots var snapshots: [SnapshotIdentifier: Snapshot] = [:] @@ -175,7 +175,7 @@ extension DiskPersistence { /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func updateStoreInfo( @_inheritActorContext updater: @Sendable @escaping (_ storeInfo: inout StoreInfo) async throws -> T - ) -> Task where AccessMode == ReadWrite { + ) -> Task where AccessMode == ReadWrite { if let storeInfo = DiskPersistenceTaskLocals.storeInfo(for: self) { return Task { var updatedStoreInfo = storeInfo @@ -223,7 +223,7 @@ extension DiskPersistence { /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func updateStoreInfo( @_inheritActorContext accessor: @Sendable @escaping (_ storeInfo: StoreInfo) async throws -> T - ) -> Task { + ) -> Task { if let storeInfo = DiskPersistenceTaskLocals.storeInfo(for: self) { return Task { try await accessor(storeInfo) } } @@ -315,7 +315,7 @@ extension DiskPersistence { func updateCurrentSnapshot( dateUpdate: ModificationUpdate = .updateOnWrite, updater: @escaping (_ snapshot: Snapshot) async throws -> T - ) -> Task where AccessMode == ReadWrite { + ) -> Task where AccessMode == ReadWrite { /// Grab access to the store info to load and update it. return updateStoreInfo { storeInfo in /// Grab the current snapshot from the store info @@ -341,7 +341,7 @@ extension DiskPersistence { /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func updateCurrentSnapshot( accessor: @escaping (_ snapshot: Snapshot) async throws -> T - ) -> Task { + ) -> Task { /// Grab access to the store info to load and update it. return updateStoreInfo { storeInfo in /// Grab the current snapshot from the store info @@ -549,7 +549,7 @@ extension DiskPersistence { public func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + transaction: @Sendable (_ transaction: any DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T ) async throws -> T { try await withoutActuallyEscaping(transaction) { escapingTransaction in /// If the transaction is starting in the context of another persistence's transaction, make sure it is a read-only one. Otherwise assert and throw an error as it likely indicates a mistake and could lead to unexpected consistency violations if one persistence succeeds while the other fails. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift index be5c02f..55f93c2 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift @@ -35,7 +35,7 @@ actor Snapshot { var cachedIteration: SnapshotIteration? /// A pointer to the last manifest updater, so updates can be serialized after the last request. - var lastUpdateManifestTask: Task? + var lastUpdateManifestTask: Task? /// The loaded datastores. var datastores: [DatastoreIdentifier: DiskPersistence.Datastore] = [:] @@ -179,7 +179,7 @@ extension Snapshot { /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func updateManifest( updater: @Sendable @escaping (_ manifest: inout SnapshotManifest, _ iteration: inout SnapshotIteration) async throws -> T - ) -> Task where AccessMode == ReadWrite { + ) -> Task where AccessMode == ReadWrite { if let (manifest, iteration) = SnapshotTaskLocals.manifest(for: persistence) { return Task { var updatedManifest = manifest @@ -247,7 +247,7 @@ extension Snapshot { /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func readManifest( accessor: @Sendable @escaping (_ manifest: SnapshotManifest, _ iteration: SnapshotIteration) async throws -> T - ) -> Task { + ) -> Task { if let (manifest, iteration) = SnapshotTaskLocals.manifest(for: persistence) { return Task { try await accessor(manifest, iteration) } diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index 7555f8c..803fe62 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -17,7 +17,7 @@ extension DiskPersistence { unowned let parent: Transaction? weak var lastReadWriteChildTransaction: Transaction? - private(set) var task: Task! + private(set) var task: Task! let transactionIndex: Int let actionName: String? let options: UnsafeTransactionOptions @@ -54,7 +54,7 @@ extension DiskPersistence { private func attachTask( options: UnsafeTransactionOptions, @_inheritActorContext handler: @Sendable @escaping () async throws -> T - ) async -> Task { + ) async -> Task { let task = Task { isActive = true let returnValue = try await TransactionTaskLocals.with(transaction: self, for: persistence) { @@ -210,7 +210,7 @@ extension DiskPersistence { actionName: String?, options: UnsafeTransactionOptions, @_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T - ) async -> (Transaction, Task) { + ) async -> (Transaction, Task) { if let parent = Self.unsafeCurrentTransaction(for: persistence) { // print("[CDS] [\(persistence.storeURL.lastPathComponent)] Found parent \(parent.transactionIndex), making child \(transactionIndex)") let (child, task) = await parent.childTransaction( @@ -246,7 +246,7 @@ extension DiskPersistence { actionName: String?, options: UnsafeTransactionOptions, @_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T - ) async -> (Transaction, Task) { + ) async -> (Transaction, Task) { assert(!self.options.contains(.readOnly) || options.contains(.readOnly), "A child transaction was declared read-write, even though its parent was read-only!") let childTransaction = Transaction( persistence: persistence, @@ -1383,14 +1383,14 @@ fileprivate protocol AnyDiskTransaction: Sendable {} fileprivate enum TransactionTaskLocals { @TaskLocal - static var transactionStorage: [ObjectIdentifier : AnyDiskTransaction] = [:] + static var transactionStorage: [ObjectIdentifier : any AnyDiskTransaction] = [:] - static func transaction(for persistence: DiskPersistence) -> AnyDiskTransaction? { + static func transaction(for persistence: DiskPersistence) -> (any AnyDiskTransaction)? { transactionStorage[ObjectIdentifier(persistence)] } static func with( - transaction: AnyDiskTransaction, + transaction: any AnyDiskTransaction, for persistence: DiskPersistence, operation: () async throws -> R ) async rethrows -> R { diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift index 6306e89..832a2d1 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift @@ -23,12 +23,12 @@ protocol TypedIdentifierProtocol: RawRepresentable, Codable, Equatable, Hashable } extension TypedIdentifierProtocol { - init(from decoder: Decoder) throws { + init(from decoder: any Decoder) throws { let rawValue = try decoder.singleValueContainer().decode(String.self) self.init(rawValue: rawValue) } - func encode(to encoder: Encoder) throws { + func encode(to encoder: any Encoder) throws { var encoder = encoder.singleValueContainer() try encoder.encode(rawValue) } diff --git a/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift b/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift index d34727f..e1a94cc 100644 --- a/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift @@ -17,7 +17,7 @@ extension MemoryPersistence { public func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + transaction: @Sendable (_ transaction: any DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T ) async throws -> T { preconditionFailure("Unimplemented") } diff --git a/Sources/CodableDatastore/Persistence/Persistence.swift b/Sources/CodableDatastore/Persistence/Persistence.swift index c9337ef..c80d41d 100644 --- a/Sources/CodableDatastore/Persistence/Persistence.swift +++ b/Sources/CodableDatastore/Persistence/Persistence.swift @@ -21,7 +21,7 @@ public protocol Persistence: Sendable { func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - @_inheritActorContext transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + @_inheritActorContext transaction: @Sendable (_ transaction: any DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T ) async throws -> T } From 579442a9e9a835fdb710bad7f406d89b985c398b Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Mon, 20 Jul 2026 05:46:29 -0700 Subject: [PATCH 09/18] Updated github workflows --- .github/workflows/api.yml | 6 +++++- .github/workflows/test.yml | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 189f232..bea2a11 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -5,6 +5,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + jobs: API-Check: name: Diagnose API Breaking Changes @@ -12,7 +16,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Source - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Mark Workspace As Safe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3a0c60..c1a62c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,13 @@ on: pull_request: branches: - main + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.ref_name != 'main' }} jobs: Test-Release: @@ -11,7 +18,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Source - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Swift Version run: | swift --version @@ -27,7 +34,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Source - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Swift Version run: | swift --version From 9082fd25dff16b14b019352d6c8dfea4a8cc23d2 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Mon, 20 Jul 2026 05:43:31 -0700 Subject: [PATCH 10/18] Added compatibility checks for Swift 6.0-main --- .github/workflows/compatibility.yml | 230 ++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 .github/workflows/compatibility.yml diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..abcc945 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,230 @@ +name: Swift Version Compatibility + +on: + pull_request: + branches: + - main + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +jobs: + Build-Release-6-0: + name: Build Swift 6.0 Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.0" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-6-0: + name: Build Swift 6.0 Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.0" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug + + Build-Release-6-1: + name: Build Swift 6.1 Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.1" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-6-1: + name: Build Swift 6.1 Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.1" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug + + Build-Release-6-2: + name: Build Swift 6.2 Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.2" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-6-2: + name: Build Swift 6.2 Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.2" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug + + Build-Release-6-3: + name: Build Swift 6.3 Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.3" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-6-3: + name: Build Swift 6.3 Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.3" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug + + Build-Release-6-4: + name: Build Swift 6.4 Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.4.x-snapshot" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-6-4: + name: Build Swift 6.4 Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.4.x-snapshot" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug + + Build-Release-main: + name: Build Swift main Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "main-snapshot" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration release + + Build-Debug-main: + name: Build Swift main Debug + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v6 + - name: Set Swift Version + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "main-snapshot" + - name: Swift Version + run: | + swift --version + - name: Build + run: | + swift build --configuration debug From 21605221710eab0be3ce5fe9a4b895c4686cd500 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Mon, 20 Jul 2026 06:04:44 -0700 Subject: [PATCH 11/18] Updated date parsing to prefer format styles when always available --- .../ISO8601DateFormatter+Milliseconds.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift index 51aac81..d171527 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift @@ -14,22 +14,31 @@ private struct GlobalDateFormatter: Sendable { static let cachedFormatter = Date.ISO8601FormatStyle(includingFractionalSeconds: true) static let parse: @Sendable (_ value: String) -> Date? = { + #if canImport(FoundationEssentials) + return { try? cachedFormatter.parse($0) } + #else if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { return { try? cachedFormatter.parse($0) } } else { return { ISO8601DateFormatter.withMilliseconds.date(from: $0) } } + #endif }() static let format: @Sendable (_ value: Date) -> String = { + #if canImport(FoundationEssentials) + return { cachedFormatter.format($0) } + #else if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { return { cachedFormatter.format($0) } } else { return { ISO8601DateFormatter.withMilliseconds.string(from: $0) } } + #endif }() } +#if !canImport(FoundationEssentials) private extension ISO8601DateFormatter { nonisolated(unsafe) static let withMilliseconds: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() @@ -44,6 +53,7 @@ private extension ISO8601DateFormatter { return formatter }() } +#endif // !canImport(FoundationEssentials) extension JSONDecoder.DateDecodingStrategy { static let iso8601WithMilliseconds: Self = custom { decoder in From 4e636d116aaee745850fa1fb82bc24145a54eeac Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Wed, 22 Jul 2026 05:16:02 -0700 Subject: [PATCH 12/18] Removed Swift 5 branches --- .../Indexes/IndexRepresentation.swift | 6 +----- .../CodableDatastore/Indexes/Indexable.swift | 21 ------------------- .../ISO8601DateFormatter+Milliseconds.swift | 2 +- .../Transaction/Transaction.swift | 7 ------- 4 files changed, 2 insertions(+), 34 deletions(-) diff --git a/Sources/CodableDatastore/Indexes/IndexRepresentation.swift b/Sources/CodableDatastore/Indexes/IndexRepresentation.swift index 187e194..ed1a755 100644 --- a/Sources/CodableDatastore/Indexes/IndexRepresentation.swift +++ b/Sources/CodableDatastore/Indexes/IndexRepresentation.swift @@ -276,9 +276,5 @@ public struct AnyIndexRepresentation: Hashable, Sendable { } } -/// Forced KeyPath conformance since Swift 5.10 doesn't support it out of the box. -#if compiler(>=6) +/// Forced KeyPath conformance since Swift 6 doesn't support it out of the box. extension KeyPath: @unchecked @retroactive Sendable where Root: Sendable, Value: Sendable {} -#else -extension KeyPath: @unchecked Sendable where Root: Sendable, Value: Sendable {} -#endif diff --git a/Sources/CodableDatastore/Indexes/Indexable.swift b/Sources/CodableDatastore/Indexes/Indexable.swift index d96ba15..329bb72 100644 --- a/Sources/CodableDatastore/Indexes/Indexable.swift +++ b/Sources/CodableDatastore/Indexes/Indexable.swift @@ -25,7 +25,6 @@ public struct AnyIndexable { #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) /// Matching implementation from https://github.com/apple/swift/pull/64899/files -#if compiler(>=6) extension Never: @retroactive Codable { public init(from decoder: any Decoder) throws { let context = DecodingError.Context( @@ -35,17 +34,6 @@ extension Never: @retroactive Codable { } public func encode(to encoder: any Encoder) throws {} } -#else -extension Never: Codable { - public init(from decoder: any Decoder) throws { - let context = DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Unable to decode an instance of Never.") - throw DecodingError.typeMismatch(Never.self, context) - } - public func encode(to encoder: any Encoder) throws {} -} -#endif #endif /// A marker protocol for types that can be used as a ranged index. @@ -92,21 +80,12 @@ extension UInt16: DiscreteIndexable, RangedIndexable {} extension UInt32: DiscreteIndexable, RangedIndexable {} extension UInt64: DiscreteIndexable, RangedIndexable {} -#if compiler(>=6) extension Optional: @retroactive Comparable where Wrapped: Comparable { public static func < (lhs: Self, rhs: Self) -> Bool { if let lhs, let rhs { return lhs < rhs } return lhs == nil && rhs != nil } } -#else -extension Optional: Comparable where Wrapped: Comparable { - public static func < (lhs: Self, rhs: Self) -> Bool { - if let lhs, let rhs { return lhs < rhs } - return lhs == nil && rhs != nil - } -} -#endif extension Optional: DiscreteIndexable where Wrapped: DiscreteIndexable {} extension Optional: RangedIndexable where Wrapped: RangedIndexable {} diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift index d171527..73a922d 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift @@ -75,7 +75,7 @@ extension JSONEncoder.DateEncodingStrategy { } } -#if compiler(>=6) && compiler(<6.2) +#if compiler(<6.2) extension ISO8601DateFormatter: @unchecked @retroactive Sendable {} extension JSONDecoder.DateDecodingStrategy: @unchecked Sendable {} extension JSONEncoder.DateEncodingStrategy: @unchecked Sendable {} diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index 803fe62..50320a6 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -1340,14 +1340,7 @@ extension DiskPersistence.Transaction { datastoreKey: DatastoreKey, bufferingPolicy limit: ObservationBufferingPolicy ) async throws -> AsyncCompactMapSequence>, ObservedEvent> { -#if swift(>=5.9) let (stream, observer) = AsyncStream.makeStream(of: ObservedEvent.self, bufferingPolicy: .init(limit)) -#else - var observer: DiskPersistence.EventObserver! - let stream = AsyncStream(ObservedEvent.self, bufferingPolicy: .init(limit)) { continuation in - observer = continuation - } -#endif let (datastore, _) = try await persistence.persistenceDatastore(for: datastoreKey) From 3b4587a882b663a0efadde19cdd9687b3ab16031 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Wed, 22 Jul 2026 05:16:26 -0700 Subject: [PATCH 13/18] Fixed a compile issue on Swift 6.0 and 6.1 --- .../CodableDatastore/Indexes/IndexRangeExpression.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift index c12e9be..1731df6 100644 --- a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift +++ b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift @@ -155,6 +155,14 @@ extension PartialRangeFrom: IndexRangeExpression { public var order: RangeOrder { .ascending } } +#if compiler(<6.2) +extension Range: @unchecked Sendable where Bound: Sendable {} +extension ClosedRange: @unchecked Sendable where Bound: Sendable {} +extension PartialRangeUpTo: @unchecked Sendable where Bound: Sendable {} +extension PartialRangeThrough: @unchecked Sendable where Bound: Sendable {} +extension PartialRangeFrom: @unchecked Sendable where Bound: Sendable {} +#endif + /// A range of indices within an Index to fetch. public struct IndexRange: IndexRangeExpression { /// The lower bound of the range. From 5954ce0bcea3084dcbdbf16175e291ab4a53f639 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 23 Jul 2026 04:57:43 -0700 Subject: [PATCH 14/18] .. --- Sources/CodableDatastore/Datastore/AsyncInstances.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/CodableDatastore/Datastore/AsyncInstances.swift b/Sources/CodableDatastore/Datastore/AsyncInstances.swift index 26ba40b..dccae3e 100644 --- a/Sources/CodableDatastore/Datastore/AsyncInstances.swift +++ b/Sources/CodableDatastore/Datastore/AsyncInstances.swift @@ -62,3 +62,12 @@ extension AsyncCompactMapSequence: AsyncInstances {} extension AsyncThrowingCompactMapSequence: AsyncInstances {} extension AsyncFilterSequence: AsyncInstances {} extension AsyncThrowingFilterSequence: AsyncInstances {} + +#if compiler(<6.1) +extension AsyncMapSequence: @unchecked Sendable where Base : Sendable, Transformed : Sendable, Base.Element : Sendable {} +extension AsyncThrowingMapSequence: @unchecked Sendable where Base : Sendable, Transformed : Sendable, Base.Element : Sendable {} +extension AsyncCompactMapSequence: @unchecked Sendable where Base : Sendable, ElementOfResult : Sendable, Base.Element : Sendable {} +extension AsyncThrowingCompactMapSequence: @unchecked Sendable where Base : Sendable, Base.Element : Sendable {} +extension AsyncFilterSequence: @unchecked Sendable where Base : Sendable, Base.Element : Sendable {} +extension AsyncThrowingFilterSequence: @unchecked Sendable where Base : Sendable, Base.Element : Sendable {} +#endif From ccaeac9ba4851e6b4c3ad2d6727ae2611bdb87dd Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 23 Jul 2026 05:20:14 -0700 Subject: [PATCH 15/18] .. --- Sources/CodableDatastore/Indexes/IndexRangeExpression.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift index 1731df6..d393faa 100644 --- a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift +++ b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift @@ -39,7 +39,11 @@ public enum RangeOrder: Equatable, Sendable { /// A type that can represent a range within an index. public protocol IndexRangeExpression: Sendable { + #if compiler(<6.1) + associatedtype Bound: Comparable + #else associatedtype Bound: Comparable & Sendable + #endif /// The definition of the lower bound of the range. var lowerBoundExpression: RangeBoundExpression { get } From 7f93c8d3e9827df6926563477e21f3a238c2bba3 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 23 Jul 2026 05:28:30 -0700 Subject: [PATCH 16/18] .. --- Sources/CodableDatastore/Datastore/AsyncInstances.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/CodableDatastore/Datastore/AsyncInstances.swift b/Sources/CodableDatastore/Datastore/AsyncInstances.swift index dccae3e..1d1c191 100644 --- a/Sources/CodableDatastore/Datastore/AsyncInstances.swift +++ b/Sources/CodableDatastore/Datastore/AsyncInstances.swift @@ -7,7 +7,11 @@ // mochidev-codable-datastore: 8A3D87799CB24B2BA7A7661369B88325 // +#if compiler(<6.1) +public protocol AsyncInstances: AsyncSequence {} +#else public protocol AsyncInstances: AsyncSequence, Sendable {} +#endif extension AsyncInstances { /// Returns the first instance of the sequence, if it exists. From 35fcf56e47b3dd8d9b86390c33b1273b1ae637c8 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 23 Jul 2026 05:32:30 -0700 Subject: [PATCH 17/18] .. --- .../Disk Persistence/Datastore/DatastorePageEntryBlock.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift index a8d0f5c..7c08a53 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift @@ -38,7 +38,7 @@ enum DatastorePageEntryBlock: Hashable, Sendable { #if canImport(Darwin) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension AsyncIteratorProtocol where Element == Byte { - #if swift(>=6.2) + #if compiler(>=6.2) @concurrent #endif @usableFromInline From d7f70c35e5f483e27838c7ed4aee47bfb0a31d57 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 23 Jul 2026 05:40:23 -0700 Subject: [PATCH 18/18] .. --- .../Indexes/IndexRangeExpression.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift index d393faa..62b9b75 100644 --- a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift +++ b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift @@ -37,13 +37,24 @@ public enum RangeOrder: Equatable, Sendable { } } +#if compiler(<6.1) /// A type that can represent a range within an index. -public protocol IndexRangeExpression: Sendable { - #if compiler(<6.1) +public protocol IndexRangeExpression { associatedtype Bound: Comparable - #else + + /// The definition of the lower bound of the range. + var lowerBoundExpression: RangeBoundExpression { get } + + /// The definition of the upper bound of the range. + var upperBoundExpression: RangeBoundExpression { get } + + ///The order the elements in the range appear. + var order: RangeOrder { get } +} +#else +/// A type that can represent a range within an index. +public protocol IndexRangeExpression: Sendable { associatedtype Bound: Comparable & Sendable - #endif /// The definition of the lower bound of the range. var lowerBoundExpression: RangeBoundExpression { get } @@ -54,6 +65,7 @@ public protocol IndexRangeExpression: Sendable { ///The order the elements in the range appear. var order: RangeOrder { get } } +#endif extension IndexRangeExpression { /// Reverse a range so it is iterated on in the opposite direction.