diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml new file mode 100644 index 0000000..2655cb8 --- /dev/null +++ b/.github/workflows/api.yml @@ -0,0 +1,23 @@ +name: API Check + +on: + pull_request: + branches: + - main + +jobs: + API-Check: + name: Diagnose API Breaking Changes + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Mark Workspace As Safe + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Diagnose API Breaking Changes + run: | + swift package diagnose-api-breaking-changes origin/main --products CodableDatastore diff --git a/Package.swift b/Package.swift index d771236..c681a0f 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.1.2")), + .package(url: "https://github.com/mochidev/AsyncSequenceReader.git", .upToNextMinor(from: "0.2.1")), .package(url: "https://github.com/mochidev/Bytes.git", .upToNextMinor(from: "0.3.0")), ], targets: [ @@ -27,11 +27,17 @@ let package = Package( dependencies: [ "AsyncSequenceReader", "Bytes" + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") ] ), .testTarget( name: "CodableDatastoreTests", - dependencies: ["CodableDatastore"] + dependencies: ["CodableDatastore"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ] ), ] ) diff --git a/README.md b/README.md index 8abf381..cff37f1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please check the [releases](https://github.com/mochidev/CodableDatastore/release dependencies: [ .package( url: "https://github.com/mochidev/CodableDatastore.git", - .upToNextMinor(from: "0.2.5") + .upToNextMinor(from: "0.3.0") ), ], ... diff --git a/Sources/CodableDatastore/Datastore/Configuration.swift b/Sources/CodableDatastore/Datastore/Configuration.swift index d55c757..597778c 100644 --- a/Sources/CodableDatastore/Datastore/Configuration.swift +++ b/Sources/CodableDatastore/Datastore/Configuration.swift @@ -3,10 +3,10 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-05-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // -public struct Configuration { +public struct Configuration: Sendable { /// The size of a single page of data on disk and in memory. /// /// Applications that deal with large objects may want to consider increasing this appropriately, diff --git a/Sources/CodableDatastore/Datastore/Datastore.swift b/Sources/CodableDatastore/Datastore/Datastore.swift index b9a19fe..8a4ec71 100644 --- a/Sources/CodableDatastore/Datastore/Datastore.swift +++ b/Sources/CodableDatastore/Datastore/Datastore.swift @@ -3,13 +3,17 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-05-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if canImport(Darwin) import Foundation +#else +@preconcurrency import Foundation +#endif /// A store for a homogenous collection of instances. -public actor Datastore { +public actor Datastore: Sendable { /// A type representing the version of the datastore within the persistence. /// /// - SeeAlso: ``DatastoreFormat/Version`` @@ -29,19 +33,19 @@ public actor Datastore { let format: Format let key: DatastoreKey let version: Version - let encoder: (_ instance: InstanceType) async throws -> Data - let decoders: [Version: (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)] + let encoder: @Sendable (_ instance: InstanceType) async throws -> Data + let decoders: [Version: @Sendable (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)] let indexRepresentations: [AnyIndexRepresentation : GeneratedIndexRepresentation] var updatedDescriptor: DatastoreDescriptor? - fileprivate var warmupStatus: TaskStatus = .waiting + fileprivate var warmupStatus: TaskStatus = .waiting fileprivate var warmupProgressHandlers: [ProgressHandler] = [] - fileprivate var storeMigrationStatus: TaskStatus = .waiting + fileprivate var storeMigrationStatus: TaskStatus = .waiting fileprivate var storeMigrationProgressHandlers: [ProgressHandler] = [] - fileprivate var indexMigrationStatus: [AnyIndexRepresentation : TaskStatus] = [:] + fileprivate var indexMigrationStatus: [AnyIndexRepresentation : TaskStatus] = [:] fileprivate var indexMigrationProgressHandlers: [AnyIndexRepresentation : ProgressHandler] = [:] public init( @@ -49,8 +53,8 @@ public actor Datastore { format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - encoder: @escaping (_ instance: InstanceType) async throws -> Data, - decoders: [Version: (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)], + encoder: @Sendable @escaping (_ instance: InstanceType) async throws -> Data, + decoders: [Version: @Sendable (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) where AccessMode == ReadWrite { self.persistence = persistence @@ -81,7 +85,7 @@ public actor Datastore { format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - decoders: [Version: (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)], + decoders: [Version: @Sendable (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) where AccessMode == ReadOnly { self.persistence = persistence @@ -124,7 +128,7 @@ extension Datastore { return descriptor } - func decoder(for version: Version) throws -> (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType) { + func decoder(for version: Version) throws -> @Sendable (_ data: Data) async throws -> (id: IdentifierType, instance: InstanceType) { guard let decoder = decoders[version] else { throw DatastoreError.missingDecoder(version: String(describing: version)) } @@ -144,14 +148,17 @@ extension Datastore { try await warmupIfNeeded(progressHandler: progressHandler) } - func warmupIfNeeded(progressHandler: ProgressHandler? = nil) async throws { + @discardableResult + func warmupIfNeeded( + @_inheritActorContext progressHandler: ProgressHandler? = nil + ) async throws -> Progress { switch warmupStatus { - case .complete: return + case .complete(let value): return value case .inProgress(let task): if let progressHandler { warmupProgressHandlers.append(progressHandler) } - try await task.value + return try await task.value case .waiting: if let progressHandler { warmupProgressHandlers.append(progressHandler) @@ -165,20 +172,20 @@ extension Datastore { } } warmupStatus = .inProgress(warmupTask) - try await warmupTask.value + return try await warmupTask.value } } - func registerAndMigrate(with transaction: DatastoreInterfaceProtocol) async throws { + func registerAndMigrate(with transaction: DatastoreInterfaceProtocol) async throws -> Progress { let persistedDescriptor = try await transaction.register(datastore: self) /// Only operate on read-write datastores beyond this point. guard let self = self as? Datastore - else { return } + else { return .complete(total: 0) } /// Make sure we have a descriptor, and that there is at least one entry, otherwise stop here. guard let persistedDescriptor, persistedDescriptor.size > 0 - else { return } + else { return .complete(total: 0) } /// Check the version to see if the current one is greater or equal to the one in the existing descriptor. If we can't decode it, stop here and throw an error — the data store is unsupported. let persistedVersion = try Version(persistedDescriptor.version) @@ -344,12 +351,15 @@ extension Datastore { } } + let completeProgress = Progress.complete(total: persistedDescriptor.size) + for handler in warmupProgressHandlers { - handler(.complete(total: persistedDescriptor.size)) + handler(completeProgress) } warmupProgressHandlers.removeAll() - warmupStatus = .complete + warmupStatus = .complete(completeProgress) + return completeProgress } } @@ -364,7 +374,13 @@ extension Datastore where AccessMode == ReadWrite { /// - index: The index to migrate. /// - minimumVersion: The minimum valid version for an index to not be migrated. /// - progressHandler: A closure that will be regularly called with progress during the migration. If no migration needs to occur, it won't be called, so setup and tear down any UI within the handler. - public func migrate>(index: KeyPath, ifLessThan minimumVersion: Version, progressHandler: ProgressHandler? = nil) async throws { + public func migrate>( + index: KeyPath, + ifLessThan minimumVersion: Version, + progressHandler: ProgressHandler? = nil + ) async throws { + let indexRepresentation = AnyIndexRepresentation(indexRepresentation: self.format[keyPath: index]) + try await persistence._withTransaction( actionName: "Migrate Entries", options: [] @@ -374,7 +390,7 @@ extension Datastore where AccessMode == ReadWrite { let descriptor = try await transaction.datastoreDescriptor(for: self.key), descriptor.size > 0, /// If we didn't declare the index, we can't do anything. This is likely an error only encountered to self-implementers of ``DatastoreFormat``'s ``DatastoreFormat/generateIndexRepresentations``. - let declaredIndex = self.indexRepresentations[AnyIndexRepresentation(indexRepresentation: self.format[keyPath: index])], + let declaredIndex = self.indexRepresentations[indexRepresentation], /// If we don't have an index stored, there is nothing to do here. This means we can skip checking it on the type. let matchingDescriptor = descriptor.directIndexes[declaredIndex.indexName.rawValue] ?? descriptor.referenceIndexes[declaredIndex.indexName.rawValue], @@ -384,9 +400,7 @@ extension Datastore where AccessMode == ReadWrite { version.rawValue < minimumVersion.rawValue else { return } - var warmUpProgress: Progress = .complete(total: 0) - try await self.warmupIfNeeded { progress in - warmUpProgress = progress + let warmUpProgress = try await self.warmupIfNeeded { progress in progressHandler?(progress.adding(current: 0, total: descriptor.size)) } @@ -396,7 +410,7 @@ extension Datastore where AccessMode == ReadWrite { let descriptor = try await transaction.datastoreDescriptor(for: self.key), descriptor.size > 0, /// If we didn't declare the index, we can't do anything. This is likely an error only encountered to self-implementers of ``DatastoreFormat``'s ``DatastoreFormat/generateIndexRepresentations``. - let declaredIndex = self.indexRepresentations[AnyIndexRepresentation(indexRepresentation: self.format[keyPath: index])], + let declaredIndex = self.indexRepresentations[indexRepresentation], /// If we don't have an index stored, there is nothing to do here. This means we can skip checking it on the type. let matchingDescriptor = descriptor.directIndexes[declaredIndex.indexName.rawValue] ?? descriptor.referenceIndexes[declaredIndex.indexName.rawValue], @@ -469,7 +483,7 @@ extension Datastore { let persistedEntry = try await transaction.primaryIndexCursor(for: identifier, datastoreKey: self.key) let entryVersion = try Version(persistedEntry.versionData) - let decoder = try await self.decoder(for: entryVersion) + let decoder = try self.decoder(for: entryVersion) let instance = try await decoder(persistedEntry.instanceData).instance return instance @@ -491,10 +505,10 @@ extension Datastore { /// - awaitWarmup: Whether the sequence should await warmup or jump right into loading. /// - Returns: An asynchronous sequence containing the instances matching the range of values in that sequence. nonisolated func _load( - _ identifierRange: some IndexRangeExpression, + _ identifierRange: some IndexRangeExpression & Sendable, order: RangeOrder, awaitWarmup: Bool - ) -> some TypedAsyncSequence<(id: IdentifierType, instance: InstanceType)> { + ) -> some TypedAsyncSequence<(id: IdentifierType, instance: InstanceType)> & Sendable { AsyncThrowingBackpressureStream { provider in if awaitWarmup { try await self.warmupIfNeeded() @@ -527,9 +541,9 @@ extension Datastore { /// - order: The order to process instances in. /// - Returns: An asynchronous sequence containing the instances matching the range of identifiers. public nonisolated func load( - _ identifierRange: some IndexRangeExpression, + _ identifierRange: some IndexRangeExpression & Sendable, order: RangeOrder = .ascending - ) -> some TypedAsyncSequence where IdentifierType: RangedIndexable { + ) -> some TypedAsyncSequence & Sendable where IdentifierType: RangedIndexable { _load(identifierRange, order: order, awaitWarmup: true) .map { $0.instance } } @@ -545,7 +559,7 @@ extension Datastore { public nonisolated func load( _ identifierRange: IndexRange, order: RangeOrder = .ascending - ) -> some TypedAsyncSequence where IdentifierType: RangedIndexable { + ) -> some TypedAsyncSequence & Sendable where IdentifierType: RangedIndexable { load(identifierRange, order: order) } @@ -559,7 +573,7 @@ extension Datastore { public nonisolated func load( _ unboundedRange: Swift.UnboundedRange, order: RangeOrder = .ascending - ) -> some TypedAsyncSequence { + ) -> some TypedAsyncSequence & Sendable { _load(IndexRange(), order: order, awaitWarmup: true) .map { $0.instance } } @@ -571,13 +585,15 @@ extension Datastore { /// - index: The index to load from. /// - Returns: An asynchronous sequence containing the instances matching the range of values in that sequence. @usableFromInline - nonisolated func _load, Range: IndexRangeExpression>( - _ range: Range, + nonisolated func _load & Sendable, Bound: Indexable & Sendable>( + _ range: some IndexRangeExpression & Sendable, order: RangeOrder = .ascending, from index: KeyPath - ) -> some TypedAsyncSequence where Range.Bound: Indexable { - AsyncThrowingBackpressureStream { provider in - guard let declaredIndex = self.indexRepresentations[AnyIndexRepresentation(indexRepresentation: self.format[keyPath: index])] + ) -> some TypedAsyncSequence & Sendable { + let declaredIndex = self.indexRepresentations[AnyIndexRepresentation(indexRepresentation: self.format[keyPath: index])] + + return AsyncThrowingBackpressureStream { provider in + guard let declaredIndex else { throw DatastoreError.missingIndex } try await self.warmupIfNeeded() @@ -639,7 +655,7 @@ extension Datastore { _ value: Index.Value, order: RangeOrder = .ascending, from index: KeyPath - ) -> some TypedAsyncSequence { + ) -> some TypedAsyncSequence & Sendable { _load(IndexRange(only: value), order: order, from: index) } @@ -674,10 +690,10 @@ extension Datastore { Value: RangedIndexable, Index: RetrievableIndexRepresentation >( - _ range: some IndexRangeExpression, + _ range: some IndexRangeExpression & Sendable, order: RangeOrder = .ascending, from index: KeyPath - ) -> some TypedAsyncSequence { + ) -> some TypedAsyncSequence & Sendable { _load(range, order: order, from: index) } @@ -699,7 +715,7 @@ extension Datastore { _ range: IndexRange, order: RangeOrder = .ascending, from index: KeyPath - ) -> some TypedAsyncSequence { + ) -> some TypedAsyncSequence & Sendable { _load(range, order: order, from: index) } @@ -717,7 +733,7 @@ extension Datastore { _ unboundedRange: Swift.UnboundedRange, order: RangeOrder = .ascending, from index: KeyPath - ) -> some TypedAsyncSequence { + ) -> some TypedAsyncSequence & Sendable { _load(IndexRange.unbounded, order: order, from: index) } } @@ -725,12 +741,12 @@ extension Datastore { // MARK: - Observation extension Datastore { - public func observe(_ idenfifier: IdentifierType) async throws -> some TypedAsyncSequence> { + public func observe(_ idenfifier: IdentifierType) async throws -> some TypedAsyncSequence> & Sendable { try await self.observe() .filter { $0.id == idenfifier } } - public func observe() async throws -> some TypedAsyncSequence> { + public func observe() async throws -> some TypedAsyncSequence> & Sendable { try await warmupIfNeeded() return try await persistence._withTransaction( @@ -783,7 +799,7 @@ extension Datastore where AccessMode == ReadWrite { let existingEntry = try await transaction.primaryIndexCursor(for: idenfifier, datastoreKey: self.key) let existingVersion = try Version(existingEntry.versionData) - let decoder = try await self.decoder(for: existingVersion) + let decoder = try self.decoder(for: existingVersion) let existingInstance = try await decoder(existingEntry.instanceData).instance return ( @@ -979,7 +995,7 @@ extension Datastore where AccessMode == ReadWrite { /// Load the instance completely so we can delete the entry within the direct and secondary indexes too. let existingVersion = try Version(existingEntry.versionData) - let decoder = try await self.decoder(for: existingVersion) + let decoder = try self.decoder(for: existingVersion) let existingInstance = try await decoder(existingEntry.instanceData).instance try await transaction.emit( @@ -1087,7 +1103,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance } @_disfavoredOverload - public func observe(_ instance: InstanceType) async throws -> some TypedAsyncSequence> { + public func observe(_ instance: InstanceType) async throws -> some TypedAsyncSequence> & Sendable { try await observe(instance.id) } } @@ -1102,7 +1118,7 @@ extension Datastore where AccessMode == ReadWrite { version: Version = Format.currentVersion, encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), - migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], + migrations: [Version : @Sendable (_ data: Data, _ decoder: JSONDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) -> Self { self.init( @@ -1111,7 +1127,7 @@ extension Datastore where AccessMode == ReadWrite { version: version, encoder: { try encoder.encode($0) }, decoders: migrations.mapValues { migration in - { data in try await migration(data, decoder) } + { @Sendable data in try await migration(data, decoder) } }, configuration: configuration ) @@ -1123,7 +1139,7 @@ extension Datastore where AccessMode == ReadWrite { key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, outputFormat: PropertyListSerialization.PropertyListFormat = .binary, - migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], + migrations: [Version : @Sendable (_ data: Data, _ decoder: PropertyListDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) -> Self { let encoder = PropertyListEncoder() @@ -1137,7 +1153,7 @@ extension Datastore where AccessMode == ReadWrite { version: version, encoder: { try encoder.encode($0) }, decoders: migrations.mapValues { migration in - { data in try await migration(data, decoder) } + { @Sendable data in try await migration(data, decoder) } }, configuration: configuration ) @@ -1151,7 +1167,7 @@ extension Datastore where AccessMode == ReadOnly { key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, decoder: JSONDecoder = JSONDecoder(), - migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], + migrations: [Version : @Sendable (_ data: Data, _ decoder: JSONDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) -> Self { self.init( @@ -1159,7 +1175,7 @@ extension Datastore where AccessMode == ReadOnly { key: key, version: version, decoders: migrations.mapValues { migration in - { data in try await migration(data, decoder) } + { @Sendable data in try await migration(data, decoder) } }, configuration: configuration ) @@ -1170,7 +1186,7 @@ extension Datastore where AccessMode == ReadOnly { format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], + migrations: [Version : @Sendable (_ data: Data, _ decoder: PropertyListDecoder) async throws -> (id: IdentifierType, instance: InstanceType)], configuration: Configuration = .init() ) -> Self { let decoder = PropertyListDecoder() @@ -1180,7 +1196,7 @@ extension Datastore where AccessMode == ReadOnly { key: key, version: version, decoders: migrations.mapValues { migration in - { data in try await migration(data, decoder) } + { @Sendable data in try await migration(data, decoder) } }, configuration: configuration ) @@ -1195,8 +1211,8 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - encoder: @escaping (_ object: InstanceType) async throws -> Data, - decoders: [Version: (_ data: Data) async throws -> InstanceType], + encoder: @Sendable @escaping (_ object: InstanceType) async throws -> Data, + decoders: [Version : @Sendable (_ data: Data) async throws -> InstanceType], configuration: Configuration = .init() ) { self.init( @@ -1205,7 +1221,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance version: version, encoder: encoder, decoders: decoders.mapValues { decoder in - { data in + { @Sendable data in let instance = try await decoder(data) return (id: instance.id, instance: instance) } @@ -1221,7 +1237,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance version: Version = Format.currentVersion, encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), - migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> InstanceType], + migrations: [Version : @Sendable (_ data: Data, _ decoder: JSONDecoder) async throws -> InstanceType], configuration: Configuration = .init() ) -> Self { self.JSONStore( @@ -1231,7 +1247,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance encoder: encoder, decoder: decoder, migrations: migrations.mapValues { migration in - { data, decoder in + { @Sendable data, decoder in let instance = try await migration(data, decoder) return (id: instance.id, instance: instance) } @@ -1246,7 +1262,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, outputFormat: PropertyListSerialization.PropertyListFormat = .binary, - migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> InstanceType], + migrations: [Version : @Sendable (_ data: Data, _ decoder: PropertyListDecoder) async throws -> InstanceType], configuration: Configuration = .init() ) -> Self { self.propertyListStore( @@ -1255,7 +1271,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance version: version, outputFormat: outputFormat, migrations: migrations.mapValues { migration in - { data, decoder in + { @Sendable data, decoder in let instance = try await migration(data, decoder) return (id: instance.id, instance: instance) } @@ -1271,7 +1287,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - decoders: [Version: (_ data: Data) async throws -> InstanceType], + decoders: [Version : @Sendable (_ data: Data) async throws -> InstanceType], configuration: Configuration = .init() ) { self.init( @@ -1279,7 +1295,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance key: key, version: version, decoders: decoders.mapValues { decoder in - { data in + { @Sendable data in let instance = try await decoder(data) return (id: instance.id, instance: instance) } @@ -1294,7 +1310,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, decoder: JSONDecoder = JSONDecoder(), - migrations: [Version: (_ data: Data, _ decoder: JSONDecoder) async throws -> InstanceType], + migrations: [Version : @Sendable (_ data: Data, _ decoder: JSONDecoder) async throws -> InstanceType], configuration: Configuration = .init() ) -> Self { self.readOnlyJSONStore( @@ -1303,7 +1319,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance version: version, decoder: decoder, migrations: migrations.mapValues { migration in - { data, decoder in + { @Sendable data, decoder in let instance = try await migration(data, decoder) return (id: instance.id, instance: instance) } @@ -1317,7 +1333,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance format: Format.Type = Format.self, key: DatastoreKey = Format.defaultKey, version: Version = Format.currentVersion, - migrations: [Version: (_ data: Data, _ decoder: PropertyListDecoder) async throws -> InstanceType], + migrations: [Version : @Sendable (_ data: Data, _ decoder: PropertyListDecoder) async throws -> InstanceType], configuration: Configuration = .init() ) -> Self { self.readOnlyPropertyListStore( @@ -1325,7 +1341,7 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance key: key, version: version, migrations: migrations.mapValues { migration in - { data, decoder in + { @Sendable data, decoder in let instance = try await migration(data, decoder) return (id: instance.id, instance: instance) } @@ -1337,8 +1353,8 @@ extension Datastore where InstanceType: Identifiable, IdentifierType == Instance // MARK: - Helper Types -private enum TaskStatus { +private enum TaskStatus { case waiting - case inProgress(Task) - case complete + case inProgress(Task) + case complete(Value) } diff --git a/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift b/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift index 8edd69e..696c83f 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreDescriptor.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-11. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -11,7 +11,7 @@ import Foundation /// A description of a ``Datastore``'s requirements of a persistence. /// /// A persistence is expected to save a description and retrieve it when a connected ``Datastore`` requests it. The ``Datastore`` then uses it to compute if indexes need to be invalidated or re-built. -public struct DatastoreDescriptor: Equatable, Hashable { +public struct DatastoreDescriptor: Equatable, Hashable, Sendable { /// The version that was current at time of serialization. /// /// If a ``Datastore`` cannot decode this version, the datastore is presumed inaccessible, and any reads or writes will fail. @@ -96,7 +96,7 @@ extension DatastoreDescriptor { /// A description of an Index used by a ``Datastore``. /// /// This information is used to determine which indexes must be invalidated or re-built, and which can be used as is. Additionally, it informs which properties must be reported along with any writes to keep existing indexes up to date. - public struct IndexDescriptor: Codable, Equatable, Hashable, Comparable { + public struct IndexDescriptor: Codable, Equatable, Hashable, Comparable, Sendable { /// The version that was first used to persist an index to disk. /// /// This is used to determine if an index must be re-built purely because something about how the index changed in a way that could not be automatically determined, such as Codable conformance changing. diff --git a/Sources/CodableDatastore/Datastore/DatastoreError.swift b/Sources/CodableDatastore/Datastore/DatastoreError.swift index 16c281c..6891e52 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreError.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreError.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-18. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Datastore/DatastoreFormat.swift b/Sources/CodableDatastore/Datastore/DatastoreFormat.swift index 7688b6c..8aca926 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreFormat.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreFormat.swift @@ -73,21 +73,21 @@ import Foundation /// - Note: If your ``Instance`` type is ``/Swift/Identifiable``, you should _not_ declare an index for `id` — special accessors are created on your behalf that can be used instead. /// /// - Important: We discourage declaring non-static stored and computed properties on your conforming type, as that will polute the key-path namespace of the format which is used for generating getters on the datastore. -public protocol DatastoreFormat { +public protocol DatastoreFormat: Sendable { /// A type representing the version of the datastore on disk. /// /// Best represented as an enum, this represents the every single version of the datastore you wish to be able to decode from disk. Assign a new version any time the codable representation or the representation of indexes is no longer backwards compatible. /// /// The various ``Datastore`` initializers take a disctionary that maps between these versions and the most up-to-date Instance type, and will provide an opportunity to use legacy representations to decode the data to the expected type. - associatedtype Version: RawRepresentable & Hashable & CaseIterable where Version.RawValue: Indexable & Comparable + associatedtype Version: RawRepresentable & Hashable & CaseIterable & Sendable where Version.RawValue: Indexable & Comparable /// The most up-to-date representation you use in your codebase. - associatedtype Instance: Codable + associatedtype Instance: Codable & Sendable /// The identifier to be used when de-duplicating instances saved in the persistence. /// /// Although ``Instance`` does _not_ need to be ``Identifiable``, a consistent identifier must still be provided for every instance to retrive and persist them. This identifier can be different from `Instance.ID` if truly necessary, though most conformers can simply set it to `Instance.ID` - associatedtype Identifier: Indexable & DiscreteIndexable + associatedtype Identifier: Indexable & DiscreteIndexable & Sendable /// A default initializer creating a format instance the datastore can use for evaluation. init() diff --git a/Sources/CodableDatastore/Datastore/DatastoreKey.swift b/Sources/CodableDatastore/Datastore/DatastoreKey.swift index ebec2f1..68cc4a2 100644 --- a/Sources/CodableDatastore/Datastore/DatastoreKey.swift +++ b/Sources/CodableDatastore/Datastore/DatastoreKey.swift @@ -3,10 +3,10 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-01. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // -public struct DatastoreKey: RawRepresentable, Hashable, Comparable { +public struct DatastoreKey: RawRepresentable, Hashable, Comparable, Sendable { public var rawValue: String public init(rawValue: String) { diff --git a/Sources/CodableDatastore/Datastore/Dictionary+RawRepresentable.swift b/Sources/CodableDatastore/Datastore/Dictionary+RawRepresentable.swift index ec0a572..66ed9ef 100644 --- a/Sources/CodableDatastore/Datastore/Dictionary+RawRepresentable.swift +++ b/Sources/CodableDatastore/Datastore/Dictionary+RawRepresentable.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-20. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Datastore/ObservedEvent.swift b/Sources/CodableDatastore/Datastore/ObservedEvent.swift index a443338..e67334f 100644 --- a/Sources/CodableDatastore/Datastore/ObservedEvent.swift +++ b/Sources/CodableDatastore/Datastore/ObservedEvent.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-12. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -44,8 +44,9 @@ public enum ObservedEvent { } extension ObservedEvent: Identifiable where IdentifierType: Hashable {} +extension ObservedEvent: Sendable where IdentifierType: Sendable, Entry: Sendable {} -public struct ObservationEntry { +public struct ObservationEntry: Sendable { var versionData: Data var instanceData: Data } diff --git a/Sources/CodableDatastore/Datastore/Progress.swift b/Sources/CodableDatastore/Datastore/Progress.swift index 794114d..6823c4e 100644 --- a/Sources/CodableDatastore/Datastore/Progress.swift +++ b/Sources/CodableDatastore/Datastore/Progress.swift @@ -3,14 +3,14 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-15. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation -public typealias ProgressHandler = (_ progress: Progress) -> Void +public typealias ProgressHandler = @Sendable (_ progress: Progress) -> Void -public enum Progress { +public enum Progress: Sendable { case evaluating case working(current: Int, total: Int) case complete(total: Int) diff --git a/Sources/CodableDatastore/Datastore/RawRepresentable+Codable.swift b/Sources/CodableDatastore/Datastore/RawRepresentable+Codable.swift index 71cc635..2ff6529 100644 --- a/Sources/CodableDatastore/Datastore/RawRepresentable+Codable.swift +++ b/Sources/CodableDatastore/Datastore/RawRepresentable+Codable.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-15. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Datastore/TypedAsyncSequence.swift b/Sources/CodableDatastore/Datastore/TypedAsyncSequence.swift index 4622df7..b372709 100644 --- a/Sources/CodableDatastore/Datastore/TypedAsyncSequence.swift +++ b/Sources/CodableDatastore/Datastore/TypedAsyncSequence.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-12. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // public protocol TypedAsyncSequence: AsyncSequence {} diff --git a/Sources/CodableDatastore/Debug/GlobalTimer.swift b/Sources/CodableDatastore/Debug/GlobalTimer.swift index 0f7ae1c..cb190ab 100644 --- a/Sources/CodableDatastore/Debug/GlobalTimer.swift +++ b/Sources/CodableDatastore/Debug/GlobalTimer.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-05. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -11,7 +11,7 @@ import Foundation actor GlobalTimer { var totalTime: TimeInterval = 0 var totalSamples: Int = 0 - static var global = GlobalTimer() + static let global = GlobalTimer() func submit(time: TimeInterval, sampleRate: Int = 100) { precondition(sampleRate > 0) diff --git a/Sources/CodableDatastore/Indexes/GeneratedIndexRepresentation.swift b/Sources/CodableDatastore/Indexes/GeneratedIndexRepresentation.swift index 0ceb560..98fc154 100644 --- a/Sources/CodableDatastore/Indexes/GeneratedIndexRepresentation.swift +++ b/Sources/CodableDatastore/Indexes/GeneratedIndexRepresentation.swift @@ -7,7 +7,7 @@ // /// A helper type for passing around metadata about an index. -public struct GeneratedIndexRepresentation { +public struct GeneratedIndexRepresentation: Sendable { /// The name the index should be serialized under. public var indexName: IndexName diff --git a/Sources/CodableDatastore/Indexes/IndexName.swift b/Sources/CodableDatastore/Indexes/IndexName.swift index 9977f88..f4dfb71 100644 --- a/Sources/CodableDatastore/Indexes/IndexName.swift +++ b/Sources/CodableDatastore/Indexes/IndexName.swift @@ -3,11 +3,11 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-20. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // /// A typed name that an index is keyed under. This is typically the path component of the key path that leads to an index. -public struct IndexName: RawRepresentable, Hashable, Comparable { +public struct IndexName: RawRepresentable, Hashable, Comparable, Sendable { public var rawValue: String public init(rawValue: String) { diff --git a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift index cf10739..6b48556 100644 --- a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift +++ b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-05. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // /// A type of bound found on either end of a range. @@ -21,7 +21,7 @@ public enum RangeBoundExpression: Equatable { extension RangeBoundExpression: Sendable where Bound: Sendable { } /// The order a range is declared in. -public enum RangeOrder: Equatable { +public enum RangeOrder: Equatable, Sendable { /// The range is in ascending order. case ascending @@ -60,7 +60,7 @@ extension IndexRangeExpression { ) } - func applying(_ newOrder: RangeOrder) -> some IndexRangeExpression { + func applying(_ newOrder: RangeOrder) -> IndexRange { IndexRange( lower: lowerBoundExpression, upper: upperBoundExpression, @@ -74,7 +74,7 @@ extension IndexRangeExpression { } /// The position relative to a range. -public enum RangePosition: Equatable { +public enum RangePosition: Equatable, Sendable { /// A value appears before the range. case before @@ -189,6 +189,8 @@ public struct IndexRange: IndexRangeExpression { } } +extension IndexRange: Sendable where Bound: Sendable {} + extension IndexRange where Bound == Never { static let unbounded = IndexRange() } @@ -199,7 +201,7 @@ postfix operator ..> extension Comparable { /// A range excluding the lower bound. @inlinable - public static func ..> (minimum: Self, maximum: Self) -> some IndexRangeExpression { + public static func ..> (minimum: Self, maximum: Self) -> IndexRange { precondition(minimum == minimum, "Range cannot have an unordered lower bound.") precondition(maximum == maximum, "Range cannot have an unordered upper bound.") precondition(minimum <= maximum, "Range lower bound must be less than upper bound.") @@ -211,7 +213,7 @@ extension Comparable { /// A partial range excluding the lower bound. @inlinable - public static postfix func ..> (minimum: Self) -> some IndexRangeExpression { + public static postfix func ..> (minimum: Self) -> IndexRange { precondition(minimum == minimum, "Range cannot have an unordered lower bound.") return IndexRange( lower: .excluding(minimum), diff --git a/Sources/CodableDatastore/Indexes/IndexRepresentation.swift b/Sources/CodableDatastore/Indexes/IndexRepresentation.swift index 5e4ae4c..19e81fc 100644 --- a/Sources/CodableDatastore/Indexes/IndexRepresentation.swift +++ b/Sources/CodableDatastore/Indexes/IndexRepresentation.swift @@ -11,9 +11,9 @@ import Foundation /// A representation of an index for a given instance, with value information erased. /// /// - Note: Although conforming to this type will construct an index, you don't be able to access this index using any of the usuall accessors. Instead, consider confirming to ``RetrievableIndexRepresentation`` or ``SingleInstanceIndexRepresentation`` as appropriate. -public protocol IndexRepresentation: Hashable { +public protocol IndexRepresentation: Hashable, Sendable { /// The instance the index belongs to. - associatedtype Instance + associatedtype Instance: Sendable /// The index type seriealized to the datastore to detect changes to index structure. var indexType: IndexType { get } @@ -32,6 +32,7 @@ extension IndexRepresentation { public var key: AnyIndexRepresentation { AnyIndexRepresentation(indexRepresentation: self) } /// Check if two ``IndexRepresentation``s are equal. + @usableFromInline func isEqual(rhs: some IndexRepresentation) -> Bool { return self == rhs as? Self } @@ -47,6 +48,7 @@ public protocol RetrievableIndexRepresentation: IndexRepresenta } extension RetrievableIndexRepresentation { + @inlinable public func valuesToIndex(for instance: Instance) -> [AnyIndexable] { valuesToIndex(for: instance).map { AnyIndexable($0)} } @@ -72,28 +74,32 @@ public protocol MultipleInputIndexRepresentation< /// /// This type of index is typically used for most unique identifiers, and may be useful if there is an alternative unique identifier a instance may be referenced under. public struct OneToOneIndexRepresentation< - Instance, + Instance: Sendable, Value: Indexable & DiscreteIndexable ->: SingleInstanceIndexRepresentation { +>: SingleInstanceIndexRepresentation, @unchecked Sendable { + @usableFromInline let keypath: KeyPath /// Initialize a One-value to One-instance index. + @inlinable public init(_ keypath: KeyPath) { self.keypath = keypath } + @inlinable public var indexType: IndexType { IndexType("OneToOneIndex(\(String(describing: Value.self)))") } - public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { + public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { guard let copy = self as? OneToOneIndexRepresentation else { return nil } return copy } + @inlinable public func valuesToIndex(for instance: Instance) -> Set { - [instance[keyPath: keypath]] + [instance[keyPath: keypath as KeyPath]] } } @@ -101,28 +107,33 @@ public struct OneToOneIndexRepresentation< /// /// This type of index is the most common, where multiple instances can share the same single value that is passed in. public struct OneToManyIndexRepresentation< - Instance, + Instance: Sendable, Value: Indexable ->: RetrievableIndexRepresentation { +>: RetrievableIndexRepresentation, @unchecked Sendable { + @usableFromInline let keypath: KeyPath /// Initialize a One-value to Many-instance index. + @inlinable public init(_ keypath: KeyPath) { self.keypath = keypath } + @inlinable public var indexType: IndexType { IndexType("OneToManyIndex(\(String(describing: Value.self)))") } - public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { + @inlinable + public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { guard let copy = self as? OneToManyIndexRepresentation else { return nil } return copy } + @inlinable public func valuesToIndex(for instance: Instance) -> Set { - [instance[keyPath: keypath]] + [instance[keyPath: keypath as KeyPath]] } } @@ -130,29 +141,34 @@ public struct OneToManyIndexRepresentation< /// /// This type of index can be used if several alternative identifiers can reference an instance, and they all reside in a single property. public struct ManyToOneIndexRepresentation< - Instance, + Instance: Sendable, Sequence: Swift.Sequence, Value: Indexable & DiscreteIndexable ->: SingleInstanceIndexRepresentation & MultipleInputIndexRepresentation { +>: SingleInstanceIndexRepresentation & MultipleInputIndexRepresentation, @unchecked Sendable { + @usableFromInline let keypath: KeyPath /// Initialize a Many-value to One-instance index. + @inlinable public init(_ keypath: KeyPath) { self.keypath = keypath } + @inlinable public var indexType: IndexType { IndexType("ManyToOneIndex(\(String(describing: Value.self)))") } - public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { + @inlinable + public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { guard let copy = self as? ManyToOneIndexRepresentation else { return nil } return copy } + @inlinable public func valuesToIndex(for instance: Instance) -> Set { - Set(instance[keyPath: keypath]) + Set(instance[keyPath: keypath as KeyPath]) } } @@ -160,29 +176,34 @@ public struct ManyToOneIndexRepresentation< /// /// This type of index is common when building relationships between different instances, where one instance may be related to several others in some way. public struct ManyToManyIndexRepresentation< - Instance, + Instance: Sendable, Sequence: Swift.Sequence, Value: Indexable ->: MultipleInputIndexRepresentation { +>: MultipleInputIndexRepresentation, @unchecked Sendable { + @usableFromInline let keypath: KeyPath /// Initialize a Many-value to Many-instance index. + @inlinable public init(_ keypath: KeyPath) { self.keypath = keypath } + @inlinable public var indexType: IndexType { IndexType("ManyToManyIndex(\(String(describing: Value.self)))") } - public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { + @inlinable + public func matches(_ instance: T.Type) -> (any IndexRepresentation)? { guard let copy = self as? ManyToManyIndexRepresentation else { return nil } return copy } + @inlinable public func valuesToIndex(for instance: Instance) -> Set { - Set(instance[keyPath: keypath]) + Set(instance[keyPath: keypath as KeyPath]) } } @@ -192,7 +213,7 @@ public struct ManyToManyIndexRepresentation< /// /// - Important: Do not include an index for `id` if your type is Identifiable — one is created automatically on your behalf. @propertyWrapper -public struct Direct { +public struct Direct: Sendable { /// The underlying value that the index will be based off of. /// /// This is ordinarily handled transparently when used as a property wrapper. @@ -201,6 +222,7 @@ public struct Direct { /// Initialize a ``Direct`` index with an initial ``IndexRepresentation`` value. /// /// This is ordinarily handled transparently when used as a property wrapper. + @inlinable public init(wrappedValue: Index) { self.wrappedValue = wrappedValue } @@ -238,13 +260,16 @@ extension Encodable { } /// A type erased index representation to be used for keying indexes in a dictionary. -public struct AnyIndexRepresentation: Hashable { +public struct AnyIndexRepresentation: Hashable, Sendable { + @usableFromInline var indexRepresentation: any IndexRepresentation + @inlinable public static func == (lhs: AnyIndexRepresentation, rhs: AnyIndexRepresentation) -> Bool { return lhs.indexRepresentation.isEqual(rhs: rhs.indexRepresentation) } + @inlinable public func hash(into hasher: inout Hasher) { hasher.combine(indexRepresentation) } diff --git a/Sources/CodableDatastore/Indexes/IndexStorage.swift b/Sources/CodableDatastore/Indexes/IndexStorage.swift index 6aee8b5..90cd500 100644 --- a/Sources/CodableDatastore/Indexes/IndexStorage.swift +++ b/Sources/CodableDatastore/Indexes/IndexStorage.swift @@ -7,7 +7,7 @@ // /// Indicates how instances are stored in the presistence. -public enum IndexStorage { +public enum IndexStorage: Sendable { /// Instances are stored in the index directly, requiring no further reads to access them. case direct diff --git a/Sources/CodableDatastore/Indexes/IndexType.swift b/Sources/CodableDatastore/Indexes/IndexType.swift index 41047d2..26e2310 100644 --- a/Sources/CodableDatastore/Indexes/IndexType.swift +++ b/Sources/CodableDatastore/Indexes/IndexType.swift @@ -3,11 +3,11 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-20. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // /// A typed name that an index is keyed under. This is typically the path component of the key path that leads to an index. -public struct IndexType: RawRepresentable, Hashable, Comparable { +public struct IndexType: RawRepresentable, Hashable, Comparable, Sendable { public var rawValue: String public init(rawValue: String) { diff --git a/Sources/CodableDatastore/Indexes/Indexable.swift b/Sources/CodableDatastore/Indexes/Indexable.swift index d08cd2d..bc8c038 100644 --- a/Sources/CodableDatastore/Indexes/Indexable.swift +++ b/Sources/CodableDatastore/Indexes/Indexable.swift @@ -9,7 +9,7 @@ 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 +public typealias Indexable = Comparable & Hashable & Codable & Sendable /// A type-erased container for Indexable values public struct AnyIndexable { @@ -43,7 +43,7 @@ extension Never: Codable { /// ```swift /// extension UUID: RangedIndexable {} /// ``` -public protocol RangedIndexable: Comparable & Hashable & Codable {} +public protocol RangedIndexable: Comparable & Hashable & Codable & Sendable {} /// A marker protocol for types that can be used as a discrete index. /// @@ -53,7 +53,7 @@ public protocol RangedIndexable: Comparable & Hashable & Codable {} /// ```swift /// extension Double: DiscreteIndexable {} /// ``` -public protocol DiscreteIndexable: Hashable & Codable {} +public protocol DiscreteIndexable: Hashable & Codable & Sendable {} // MARK: - Swift Standard Library Conformances @@ -89,5 +89,9 @@ extension Optional: RangedIndexable where Wrapped: RangedIndexable {} // MARK: - Foundation Conformances extension Date: RangedIndexable {} +#if canImport(Darwin) extension Decimal: RangedIndexable {} +#else +extension Decimal: RangedIndexable, @unchecked Sendable {} +#endif extension UUID: DiscreteIndexable {} diff --git a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift index c18d753..d86dea3 100644 --- a/Sources/CodableDatastore/Indexes/UUID+Comparable.swift +++ b/Sources/CodableDatastore/Indexes/UUID+Comparable.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-04. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/AccessMode.swift b/Sources/CodableDatastore/Persistence/AccessMode.swift index 4d65b1d..44ede4f 100644 --- a/Sources/CodableDatastore/Persistence/AccessMode.swift +++ b/Sources/CodableDatastore/Persistence/AccessMode.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-05-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // /// An AccessMode marker type. diff --git a/Sources/CodableDatastore/Persistence/Cursor.swift b/Sources/CodableDatastore/Persistence/Cursor.swift index e5f88dc..c934f96 100644 --- a/Sources/CodableDatastore/Persistence/Cursor.swift +++ b/Sources/CodableDatastore/Persistence/Cursor.swift @@ -3,13 +3,13 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-17. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // /// An opaque type ``Persistence``s may use to indicate a position in their storage. /// /// - Note: A cursor is only valid within the same transaction for the same persistence it was created for. -public protocol CursorProtocol

{ +public protocol CursorProtocol

: Sendable { associatedtype P: Persistence var persistence: P { get } diff --git a/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift b/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift index 92441dc..332e1e7 100644 --- a/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift +++ b/Sources/CodableDatastore/Persistence/DatastoreInterfaceError.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-13. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift b/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift index 90fc4e4..bb4d464 100644 --- a/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift +++ b/Sources/CodableDatastore/Persistence/DatastoreInterfaceProtocol.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-29. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -11,7 +11,7 @@ import Foundation /// A interface a ``Datastore`` uses to communicate with a ``Persistence``. /// /// This protocol is provided so others can implement new persistences modelled after the ones provided by ``CodableDatastore``. You should never call any of these methods directly. -public protocol DatastoreInterfaceProtocol { +public protocol DatastoreInterfaceProtocol: Sendable { // MARK: Registration /// Register a ``Datastore`` with a ``Persistence`` so that it can be informed of changes made to the persistence. @@ -137,23 +137,23 @@ public protocol DatastoreInterfaceProtocol { // MARK: Range Lookups func primaryIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, datastoreKey: DatastoreKey, - instanceConsumer: (_ versionData: Data, _ instanceData: Data) async throws -> () + instanceConsumer: @Sendable (_ versionData: Data, _ instanceData: Data) async throws -> () ) async throws func directIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, indexName: IndexName, datastoreKey: DatastoreKey, - instanceConsumer: (_ versionData: Data, _ instanceData: Data) async throws -> () + instanceConsumer: @Sendable (_ versionData: Data, _ instanceData: Data) async throws -> () ) async throws func secondaryIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, indexName: IndexName, datastoreKey: DatastoreKey, - identifierConsumer: (_ identifier: IdentifierType) async throws -> () + identifierConsumer: @Sendable (_ identifier: IdentifierType) async throws -> () ) async throws // MARK: Mutations @@ -289,7 +289,7 @@ public protocol DatastoreInterfaceProtocol { // MARK: - Helper Types /// A strategy that handles exhaustion of a buffer’s capacity. -public enum ObservationBufferingPolicy { +public enum ObservationBufferingPolicy: Hashable, Sendable { /// Continue to add to the buffer, treating its capacity as infinite. case unbounded diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift index 60c0d75..85117bd 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/AsyncThrowingBackpressureStream.swift @@ -3,12 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation -struct AsyncThrowingBackpressureStream { +struct AsyncThrowingBackpressureStream: Sendable { fileprivate actor StateMachine { var pendingEvents: [(CheckedContinuation, Result)] = [] var eventsReadyContinuation: CheckedContinuation? @@ -80,7 +80,7 @@ struct AsyncThrowingBackpressureStream { private var stateMachine: StateMachine - init(provider: @escaping (Continuation) async throws -> ()) { + init(provider: @Sendable @escaping (Continuation) async throws -> ()) { stateMachine = StateMachine() let continuation = Continuation(stateMachine: stateMachine) diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift index 4ee7944..a46ef07 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift @@ -3,10 +3,14 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-23. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if canImport(Darwin) import Foundation +#else +@preconcurrency import Foundation +#endif import Bytes typealias DatastoreIndexIdentifier = TypedIdentifier.Datastore.Index> @@ -291,8 +295,8 @@ extension DiskPersistence.Datastore.Index { func pageIndex( for proposedEntry: T, in pages: [DatastoreIndexManifest.PageInfo], - pageBuilder: (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, - comparator: (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder + pageBuilder: @Sendable (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, + comparator: @Sendable (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder ) async throws -> Int? { var slice = pages[...] @@ -384,7 +388,7 @@ extension DiskPersistence.Datastore.Index { func entry( for proposedEntry: T, - comparator: (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder + comparator: @Sendable (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder ) async throws -> ( cursor: DiskPersistence.InstanceCursor, entry: DatastorePageEntry @@ -400,8 +404,8 @@ extension DiskPersistence.Datastore.Index { func entry( for proposedEntry: T, in pages: [DatastoreIndexManifest.PageInfo], - pageBuilder: (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, - comparator: (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder + pageBuilder: @Sendable (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, + comparator: @Sendable (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder ) async throws -> ( cursor: DiskPersistence.InstanceCursor, entry: DatastorePageEntry @@ -523,7 +527,7 @@ extension DiskPersistence.Datastore.Index { page = await datastore.page(for: .init(index: self.id, page: pageID)) } - let blocks = try await page.blocks.reduce(into: []) { $0.append($1) } + let blocks = try await Array(page.blocks) guard !blocks.isEmpty else { throw DiskPersistenceError.invalidPageFormat } return DiskPersistence.InsertionCursor( @@ -545,7 +549,7 @@ extension DiskPersistence.Datastore.Index { func insertionCursor( for proposedEntry: T, - comparator: (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder + comparator: @Sendable (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder ) async throws -> DiskPersistence.InsertionCursor { try await insertionCursor( for: proposedEntry, @@ -558,8 +562,8 @@ extension DiskPersistence.Datastore.Index { func insertionCursor( for proposedEntry: T, in pages: [DatastoreIndexManifest.PageInfo], - pageBuilder: (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, - comparator: (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder + pageBuilder: @Sendable (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, + comparator: @Sendable (_ lhs: T, _ rhs: DatastorePageEntry) throws -> SortOrder ) async throws -> DiskPersistence.InsertionCursor { /// Get the page the entry should reside on guard @@ -682,7 +686,7 @@ extension DiskPersistence.Datastore.Index { extension DiskPersistence.Datastore.Index { func forwardScanEntries( after startCursor: DiskPersistence.InsertionCursor, - entryHandler: (_ entry: DatastorePageEntry) async throws -> Bool + entryHandler: @Sendable (_ entry: DatastorePageEntry) async throws -> Bool ) async throws { try await forwardScanEntries( after: startCursor, @@ -695,8 +699,8 @@ extension DiskPersistence.Datastore.Index { func forwardScanEntries( after startCursor: DiskPersistence.InsertionCursor, in pages: [DatastoreIndexManifest.PageInfo], - pageBuilder: (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, - entryHandler: (_ entry: DatastorePageEntry) async throws -> Bool + pageBuilder: @Sendable (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, + entryHandler: @Sendable (_ entry: DatastorePageEntry) async throws -> Bool ) async throws { guard startCursor.datastore === datastore, @@ -757,7 +761,7 @@ extension DiskPersistence.Datastore.Index { func backwardScanEntries( before startCursor: DiskPersistence.InsertionCursor, - entryHandler: (_ entry: DatastorePageEntry) async throws -> Bool + entryHandler: @Sendable (_ entry: DatastorePageEntry) async throws -> Bool ) async throws { try await backwardScanEntries( before: startCursor, @@ -770,8 +774,8 @@ extension DiskPersistence.Datastore.Index { func backwardScanEntries( before startCursor: DiskPersistence.InsertionCursor, in pages: [DatastoreIndexManifest.PageInfo], - pageBuilder: (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, - entryHandler: (_ entry: DatastorePageEntry) async throws -> Bool + pageBuilder: @Sendable (_ pageID: DatastorePageIdentifier) async -> DiskPersistence.Datastore.Page, + entryHandler: @Sendable (_ entry: DatastorePageEntry) async throws -> Bool ) async throws { guard startCursor.datastore === datastore, @@ -797,7 +801,7 @@ extension DiskPersistence.Datastore.Index { blockCountToInclude = blockIndex + 1 } - let blocks = try await page.blocks.prefix(blockCountToInclude).reduce(into: []) { $0.append($1) } + let blocks = try await Array(page.blocks.prefix(blockCountToInclude)) for block in blocks.reversed() { switch block { @@ -896,7 +900,7 @@ extension DiskPersistence.Datastore.Index { let existingPage = insertAfter.page /// Split the existing page into two halves. - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) let firstHalf = existingPageBlocks[...insertAfter.blockIndex] let remainingBlocks = existingPageBlocks[(insertAfter.blockIndex+1)...] @@ -969,7 +973,7 @@ extension DiskPersistence.Datastore.Index { if attemptPageCollation { /// Load the first page to see how large it is compared to the amount of space we have left on our final new page let existingPage = await datastore.page(for: .init(index: id, page: pageID)) - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) /// Calculate how much space remains on the final new page, and insert the existing blocks if they all fit. /// Note that we are guaranteed to have at least one new page by this point, since we are inserting and not replacing. @@ -1118,7 +1122,7 @@ extension DiskPersistence.Datastore.Index { /// If this is our first time reaching this point, we have some new blocks to insert. if index <= lastInstanceBlock.pageIndex { let existingPage = await datastore.page(for: .init(index: self.id, page: pageID)) - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) /// Grab the index range that we are replacing on this page let startingIndex = index == firstInstanceBlock.pageIndex ? firstInstanceBlock.blockIndex : 0 @@ -1194,7 +1198,7 @@ extension DiskPersistence.Datastore.Index { /// Load the first page to see how large it is compared to the amount of space we have left on our final new page let existingPage = await datastore.page(for: .init(index: id, page: pageID)) - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) /// Calculate how much space remains on the final new page, and insert the existing blocks if they all fit. /// Note that we are guaranteed to have at least one new page by this point, since we are inserting and not replacing. @@ -1314,7 +1318,7 @@ extension DiskPersistence.Datastore.Index { /// If this is our first time reaching this point, we have some new blocks to insert. if index <= lastInstanceBlock.pageIndex { let existingPage = await datastore.page(for: .init(index: self.id, page: pageID)) - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) /// Grab the index range that we are replacing on this page let startingIndex = index == firstInstanceBlock.pageIndex ? firstInstanceBlock.blockIndex : 0 @@ -1363,7 +1367,7 @@ extension DiskPersistence.Datastore.Index { /// Load the first page to see how large it is compared to the amount of space we have left on our final new page let existingPage = await datastore.page(for: .init(index: id, page: pageID)) - let existingPageBlocks = try await existingPage.blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let existingPageBlocks = try await Array(existingPage.blocks) /// Calculate how much space remains on the final new page, and insert the existing blocks if they all fit. /// Note that we are guaranteed to have at least one new page by this point, since we are inserting and not replacing. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift index 430d858..4d45933 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndexManifest.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-26. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift index 1e3761b..5bda3c7 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePage.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-23. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -107,6 +107,27 @@ extension DiskPersistence.Datastore.Page { } } + private nonisolated func performRead(sequence: AnyReadableSequence) async throws -> MultiplexedAsyncSequence> { + var iterator = sequence.makeAsyncIterator() + + try await iterator.check(Self.header) + + /// Pages larger than 1 GB are unsupported. + let transformation = try await iterator.collect(max: 1024*1024*1024) { sequence in + sequence.iteratorMap { iterator in + guard let block = try await iterator.next(DatastorePageEntryBlock.self) + else { throw DiskPersistenceError.invalidPageFormat } + return block + } + } + + if let transformation { + return MultiplexedAsyncSequence(base: AnyReadableSequence(transformation)) + } else { + return MultiplexedAsyncSequence(base: AnyReadableSequence([])) + } + } + var blocks: MultiplexedAsyncSequence> { get async throws { if let blocksReaderTask { @@ -114,26 +135,7 @@ extension DiskPersistence.Datastore.Page { } let readerTask = Task { - let sequence = try readableSequence - - var iterator = sequence.makeAsyncIterator() - - try await iterator.check(Self.header) - - /// Pages larger than 1 GB are unsupported. - let transformation = try await iterator.collect(max: 1024*1024*1024) { sequence in - sequence.iteratorMap { iterator in - guard let block = try await iterator.next(DatastorePageEntryBlock.self) - else { throw DiskPersistenceError.invalidPageFormat } - return block - } - } - - if let transformation { - return MultiplexedAsyncSequence(base: AnyReadableSequence(transformation)) - } else { - return MultiplexedAsyncSequence(base: AnyReadableSequence([])) - } + try await performRead(sequence: try readableSequence) } isPersisted = true blocksReaderTask = readerTask @@ -145,7 +147,7 @@ extension DiskPersistence.Datastore.Page { func persistIfNeeded() async throws { guard !isPersisted else { return } - let blocks = try await blocks.reduce(into: [DatastorePageEntryBlock]()) { $0.append($1) } + let blocks = try await Array(blocks) let bytes = blocks.reduce(into: Self.header) { $0.append(contentsOf: $1.bytes) } let pageURL = pageURL @@ -162,13 +164,13 @@ extension DiskPersistence.Datastore.Page { static var headerSize: Int { header.count } } -actor MultiplexedAsyncSequence: AsyncSequence { +actor MultiplexedAsyncSequence: AsyncSequence where Base.Element: Sendable, Base.AsyncIterator: Sendable, Base.AsyncIterator.Element: Sendable { typealias Element = Base.Element private var cachedEntries: [Task] = [] private var baseIterator: Base.AsyncIterator - struct AsyncIterator: AsyncIteratorProtocol { + struct AsyncIterator: AsyncIteratorProtocol & Sendable { let base: MultiplexedAsyncSequence var index: Array.Index = 0 @@ -194,8 +196,7 @@ actor MultiplexedAsyncSequence: AsyncSequence { _ = try? await lastTask?.value /// Grab the next iteration, and save a reference back to it. This is only safe since we chain the requests behind previous ones. - var iteratorCopy = baseIterator - let nextEntry = try await iteratorCopy.next() + let (nextEntry, iteratorCopy) = try await nextBase(iterator: baseIterator) baseIterator = iteratorCopy return nextEntry @@ -205,6 +206,12 @@ actor MultiplexedAsyncSequence: AsyncSequence { } } + nonisolated func nextBase(iterator: Base.AsyncIterator) async throws -> (Element?, Base.AsyncIterator) { + var iteratorCopy = iterator + let nextEntry = try await iteratorCopy.next() + return (nextEntry, iteratorCopy) + } + nonisolated func makeAsyncIterator() -> AsyncIterator { AsyncIterator(base: self) } @@ -213,3 +220,11 @@ actor MultiplexedAsyncSequence: AsyncSequence { baseIterator = base.makeAsyncIterator() } } + +extension RangeReplaceableCollection { + init(_ sequence: 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/DatastorePageEntry.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift index 75d46ed..b8514e1 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntry.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-27. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift index f881a22..26d698f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastorePageEntryBlock.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-27. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -12,7 +12,7 @@ import Bytes /// A block of data that represents a portion of an entry on a page. @usableFromInline -enum DatastorePageEntryBlock: Hashable { +enum DatastorePageEntryBlock: Hashable, Sendable { /// The tail end of an entry. /// /// This must be combined with a previous block to form an entry. diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift index 2a665cb..a283a3f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-22. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRootManifest.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRootManifest.swift index 9ed249d..9b9c068 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRootManifest.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRootManifest.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-14. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift index 1e0f377..e4f2ca2 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/PersistenceDatastore.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -26,7 +26,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/DatedIdentifier.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DatedIdentifier.swift index 9f5f21a..e6537e1 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DatedIdentifier.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DatedIdentifier.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-08. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -104,3 +104,7 @@ private extension DateFormatter { return formatter }() } + +#if !canImport(Darwin) +extension DateFormatter: @unchecked Sendable {} +#endif diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift index 4c06c59..2e97d27 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift @@ -3,10 +3,14 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-03. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if canImport(Darwin) import Foundation +#else +@preconcurrency import Foundation +#endif public actor DiskPersistence: Persistence { /// The location of this persistence. @@ -16,7 +20,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] = [:] @@ -169,8 +173,9 @@ extension DiskPersistence { /// - Note: Calling this method when no store info exists on disk will create it, even if no changes occur in the block. /// - Parameter updater: An updater that takes a mutable reference to a store info, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. - func updateStoreInfo(updater: @escaping (_ storeInfo: inout StoreInfo) async throws -> T) -> Task where AccessMode == ReadWrite { - + func updateStoreInfo( + @_inheritActorContext updater: @Sendable @escaping (_ storeInfo: inout StoreInfo) async throws -> T + ) -> Task where AccessMode == ReadWrite { if let storeInfo = DiskPersistenceTaskLocals.storeInfo { return Task { var updatedStoreInfo = storeInfo @@ -216,8 +221,9 @@ extension DiskPersistence { /// /// - Parameter accessor: An accessor that takes an immutable reference to a store info, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. - func updateStoreInfo(accessor: @escaping (_ storeInfo: StoreInfo) async throws -> T) -> Task { - + func updateStoreInfo( + @_inheritActorContext accessor: @Sendable @escaping (_ storeInfo: StoreInfo) async throws -> T + ) -> Task { if let storeInfo = DiskPersistenceTaskLocals.storeInfo { return Task { try await accessor(storeInfo) } } @@ -249,8 +255,12 @@ extension DiskPersistence { /// - Note: Calling this method when no store info exists on disk will create it, even if no changes occur in the block. /// - Parameter updater: An updater that takes a mutable reference to a store info, and will forward the returned value to the caller. /// - Returns: The value returned from the `updater`. - func withStoreInfo(updater: @escaping (_ storeInfo: inout StoreInfo) async throws -> T) async throws -> T where AccessMode == ReadWrite { - try await updateStoreInfo(updater: updater).value + func withStoreInfo( + updater: @Sendable (_ storeInfo: inout StoreInfo) async throws -> T + ) async throws -> T where AccessMode == ReadWrite { + try await withoutActuallyEscaping(updater) { escapingClosure in + try await updateStoreInfo(updater: escapingClosure).value + } } /// Load the store info in an updater. @@ -260,8 +270,12 @@ extension DiskPersistence { /// - Parameter accessor: An accessor that takes an immutable reference to a store info, and will forward the returned value to the caller. /// - Returns: The value returned from the `accessor`. @_disfavoredOverload - func withStoreInfo(accessor: @escaping (_ storeInfo: StoreInfo) async throws -> T) async throws -> T where AccessMode == ReadOnly { - try await updateStoreInfo(accessor: accessor).value + func withStoreInfo( + accessor: @Sendable (_ storeInfo: StoreInfo) async throws -> T + ) async throws -> T where AccessMode == ReadOnly { + try await withoutActuallyEscaping(accessor) { escapingClosure in + try await updateStoreInfo(accessor: escapingClosure).value + } } } @@ -289,7 +303,7 @@ extension DiskPersistence { /// - Parameter dateUpdate: The method to which to update the date of the main store with. /// - Parameter updater: An updater that takes a reference to the current ``Snapshot``, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. - func updateCurrentSnapshot( + func updateCurrentSnapshot( dateUpdate: ModificationUpdate = .updateOnWrite, updater: @escaping (_ snapshot: Snapshot) async throws -> T ) -> Task where AccessMode == ReadWrite { @@ -316,7 +330,7 @@ extension DiskPersistence { /// /// - Parameter accessor: An accessor that takes a reference to the current ``Snapshot``, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. - func updateCurrentSnapshot( + func updateCurrentSnapshot( accessor: @escaping (_ snapshot: Snapshot) async throws -> T ) -> Task { /// Grab access to the store info to load and update it. @@ -337,11 +351,13 @@ extension DiskPersistence { /// - Parameter dateUpdate: The method to which to update the date of the main store with. /// - Parameter updater: An updater that takes a reference to the current ``Snapshot``, and will forward the returned value to the caller. /// - Returns: The value returned from the `accessor`. - func updatingCurrentSnapshot( + func updatingCurrentSnapshot( dateUpdate: ModificationUpdate = .updateOnWrite, - updater: @escaping (_ snapshot: Snapshot) async throws -> T + updater: @Sendable (_ snapshot: Snapshot) async throws -> T ) async throws -> T where AccessMode == ReadWrite { - try await updateCurrentSnapshot(dateUpdate: dateUpdate, updater: updater).value + try await withoutActuallyEscaping(updater) { escapingClosure in + try await updateCurrentSnapshot(dateUpdate: dateUpdate, updater: escapingClosure).value + } } /// Load the current snapshot in an accessor. @@ -353,10 +369,12 @@ extension DiskPersistence { /// - Parameter accessor: An accessor that takes a reference to the current ``Snapshot``, and will forward the returned value to the caller. /// - Returns: The value returned from the `accessor`. @_disfavoredOverload - func readingCurrentSnapshot( - accessor: @escaping (_ snapshot: Snapshot) async throws -> T + func readingCurrentSnapshot( + accessor: @Sendable (_ snapshot: Snapshot) async throws -> T ) async throws -> T { - try await updateCurrentSnapshot(accessor: accessor).value + try await withoutActuallyEscaping(accessor) { escapingClosure in + try await updateCurrentSnapshot(accessor: escapingClosure).value + } } } @@ -449,25 +467,27 @@ extension DiskPersistence { // MARK: - Transactions extension DiskPersistence { - public func _withTransaction( + public func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - transaction: @escaping (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T ) async throws -> T { - let (transaction, task) = await Transaction.makeTransaction( - persistence: self, - lastTransaction: lastTransaction, - actionName: actionName, options: options - ) { interface, isDurable in - try await transaction(interface, isDurable) - } - - /// Save the last non-concurrent top-level transaction from the list. Note that disk persistence currently does not support concurrent idempotent transactions. - if !options.contains(.readOnly), transaction.parent == nil { - lastTransaction = transaction + try await withoutActuallyEscaping(transaction) { escapingTransaction in + let (transaction, task) = await Transaction.makeTransaction( + persistence: self, + lastTransaction: lastTransaction, + actionName: actionName, options: options + ) { interface, isDurable in + try await escapingTransaction(interface, isDurable) + } + + /// Save the last non-concurrent top-level transaction from the list. Note that disk persistence currently does not support concurrent idempotent transactions. + if !options.contains(.readOnly), transaction.parent == nil { + lastTransaction = transaction + } + + return try await task.value } - - return try await task.value } func persist( diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift index 913a24c..0fbf03f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistenceError.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-09. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift index 4c85976..d898905 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/ISO8601DateFormatter+Milliseconds.swift @@ -3,13 +3,13 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-07. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation extension ISO8601DateFormatter { - static var withMilliseconds: ISO8601DateFormatter = { + static let withMilliseconds: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.formatOptions = [ @@ -42,3 +42,9 @@ extension JSONEncoder.DateEncodingStrategy { try container.encode(string) } } + +#if !canImport(Darwin) +extension ISO8601DateFormatter: @unchecked Sendable {} +extension JSONDecoder.DateDecodingStrategy: @unchecked Sendable {} +extension JSONEncoder.DateEncodingStrategy: @unchecked Sendable {} +#endif diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/JSONCoder.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/JSONCoder.swift index 9420ded..df81289 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/JSONCoder.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/JSONCoder.swift @@ -3,13 +3,13 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-14. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation extension JSONEncoder { - static var shared: JSONEncoder = { + static let shared: JSONEncoder = { let datastoreEncoder = JSONEncoder() datastoreEncoder.dateEncodingStrategy = .iso8601WithMilliseconds #if DEBUG @@ -22,9 +22,14 @@ extension JSONEncoder { } extension JSONDecoder { - static var shared: JSONDecoder = { + static let shared: JSONDecoder = { let datastoreDecoder = JSONDecoder() datastoreDecoder.dateDecodingStrategy = .iso8601WithMilliseconds return datastoreDecoder }() } + +#if !canImport(Darwin) +extension JSONEncoder: @unchecked Sendable {} +extension JSONDecoder: @unchecked Sendable {} +#endif diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/LazyTask.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/LazyTask.swift index b8a7664..4c1c9d9 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/LazyTask.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/LazyTask.swift @@ -3,11 +3,11 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-05. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // struct LazyTask { - let factory: () async -> T + let factory: @Sendable () async -> T var value: T { get async { @@ -16,8 +16,10 @@ struct LazyTask { } } +extension LazyTask: Sendable where T: Sendable {} + struct LazyThrowingTask { - let factory: () async throws -> T + let factory: @Sendable () async throws -> T var value: T { get async throws { @@ -25,3 +27,5 @@ struct LazyThrowingTask { } } } + +extension LazyThrowingTask: Sendable where T: Sendable {} diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift index c55eeef..9b3691d 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/Snapshot.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-09. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -34,7 +34,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] = [:] @@ -174,7 +174,7 @@ extension Snapshot { /// - Parameter updater: An updater that takes a mutable reference to a manifest, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func updateManifest( - updater: @escaping (_ manifest: inout SnapshotManifest, _ iteration: inout SnapshotIteration) async throws -> T + updater: @Sendable @escaping (_ manifest: inout SnapshotManifest, _ iteration: inout SnapshotIteration) async throws -> T ) -> Task where AccessMode == ReadWrite { if let (manifest, iteration) = SnapshotTaskLocals.manifest { return Task { @@ -242,7 +242,7 @@ extension Snapshot { /// - Parameter accessor: An accessor that takes an immutable reference to a manifest, and will forward the returned value to the caller. /// - Returns: A ``/Swift/Task`` which contains the value of the updater upon completion. func readManifest( - accessor: @escaping (_ manifest: SnapshotManifest, _ iteration: SnapshotIteration) async throws -> T + accessor: @Sendable @escaping (_ manifest: SnapshotManifest, _ iteration: SnapshotIteration) async throws -> T ) -> Task { if let (manifest, iteration) = SnapshotTaskLocals.manifest { @@ -285,10 +285,12 @@ extension Snapshot { /// - Note: Calling this method when no manifest exists on disk will create it, even if no changes occur in the block. /// - Parameter updater: An updater that takes a mutable reference to a manifest, and will forward the returned value to the caller. /// - Returns: The value returned from the `updater`. - func updatingManifest( - updater: @escaping (_ manifest: inout SnapshotManifest, _ iteration: inout SnapshotIteration) async throws -> T + func updatingManifest( + updater: @Sendable (_ manifest: inout SnapshotManifest, _ iteration: inout SnapshotIteration) async throws -> T ) async throws -> T where AccessMode == ReadWrite { - try await updateManifest(updater: updater).value + try await withoutActuallyEscaping(updater) { escapingClosure in + try await updateManifest(updater: escapingClosure).value + } } /// Load the manifest in an updater. @@ -298,10 +300,12 @@ extension Snapshot { /// - Parameter accessor: An accessor that takes an immutable reference to a manifest, and will forward the returned value to the caller. /// - Returns: The value returned from the `accessor`. @_disfavoredOverload - func readingManifest( - accessor: @escaping (_ manifest: SnapshotManifest, _ iteration: SnapshotIteration) async throws -> T + func readingManifest( + accessor: @Sendable (_ manifest: SnapshotManifest, _ iteration: SnapshotIteration) async throws -> T ) async throws -> T { - try await readManifest(accessor: accessor).value + try await withoutActuallyEscaping(accessor) { escapingClosure in + try await readManifest(accessor: escapingClosure).value + } } } diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift index e3eb3af..3c32d08 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-15. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotManifest.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotManifest.swift index 00b42ff..b1fadeb 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotManifest.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotManifest.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-08. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -11,12 +11,12 @@ import Foundation /// Versions supported by ``DiskPersisitence``. /// /// These are used when dealing with format changes at the library level. -enum SnapshotManifestVersion: String, Codable { +enum SnapshotManifestVersion: String, Codable, Sendable { case alpha } /// A struct to store information about a ``DiskPersistence``'s snapshot on disk. -struct SnapshotManifest: Codable, Equatable, Identifiable { +struct SnapshotManifest: Codable, Equatable, Identifiable, Sendable { /// The version of the snapshot, used when dealing with format changes at the library level. var version: SnapshotManifestVersion = .alpha diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift index 1e9a866..d3eb7dd 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-03. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // enum SortOrder { diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/StoreInfo/StoreInfo.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/StoreInfo/StoreInfo.swift index 2c95c49..77cdd7e 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/StoreInfo/StoreInfo.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/StoreInfo/StoreInfo.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-07. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -11,12 +11,12 @@ import Foundation /// Versions supported by ``DiskPersisitence``. /// /// These are used when dealing with format changes at the library level. -enum StoreInfoVersion: String, Codable { +enum StoreInfoVersion: String, Codable, Sendable { case alpha } /// A struct to store information about a ``DiskPersistence`` on disk. -struct StoreInfo: Codable, Equatable { +struct StoreInfo: Codable, Equatable, Sendable { /// The version of the persistence, used when dealing with format changes at the library level. var version: StoreInfoVersion = .alpha diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/DiskCursor.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/DiskCursor.swift index 32d7e12..339f88a 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/DiskCursor.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/DiskCursor.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-03. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // extension DiskPersistence { diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index dcc1fce..777b6ff 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-21. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -49,7 +49,7 @@ extension DiskPersistence { private func attachTask( options: UnsafeTransactionOptions, - handler: @escaping () async throws -> T + @_inheritActorContext handler: @Sendable @escaping () async throws -> T ) async -> Task { let task = Task { isActive = true @@ -201,7 +201,7 @@ extension DiskPersistence { lastTransaction: Transaction?, actionName: String?, options: UnsafeTransactionOptions, - handler: @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T + @_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T ) async -> (Transaction, Task) { if let parent = Self.unsafeCurrentTransaction { let (child, task) = await parent.childTransaction(options: options, handler: handler) @@ -228,7 +228,7 @@ extension DiskPersistence { func childTransaction( options: UnsafeTransactionOptions, - handler: @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T + @_inheritActorContext handler: @Sendable @escaping (_ transaction: Transaction, _ isDurable: Bool) async throws -> T ) 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 transaction = Transaction( @@ -462,6 +462,7 @@ extension DiskPersistence.Transaction: DatastoreInterfaceProtocol { // MARK: - Cursor Lookups +@Sendable private func primaryIndexComparator(lhs: IdentifierType, rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 2 else { throw DiskPersistenceError.invalidEntryFormat } @@ -473,6 +474,7 @@ private func primaryIndexComparator(lhs: IdentifierTy return lhs.sortOrder(comparedTo: entryIdentifier) } +@Sendable private func directIndexComparator(lhs: (index: IndexType, identifier: IdentifierType), rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 3 else { throw DiskPersistenceError.invalidEntryFormat } @@ -491,6 +493,7 @@ private func directIndexComparator(lhs: (index: IndexType, identifier: IdentifierType), rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 1 else { throw DiskPersistenceError.invalidEntryFormat } @@ -635,6 +638,7 @@ extension DiskPersistence.Transaction { // MARK: - Range Lookups +@Sendable private func primaryIndexBoundComparator(lhs: (bound: RangeBoundExpression, order: RangeOrder), rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 2 else { throw DiskPersistenceError.invalidEntryFormat } @@ -647,6 +651,7 @@ private func primaryIndexBoundComparator(lhs: (bound: return lhs.bound.sortOrder(comparedTo: entryIdentifier, order: lhs.order) } +@Sendable private func directIndexBoundComparator(lhs: (bound: RangeBoundExpression, order: RangeOrder), rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 3 else { throw DiskPersistenceError.invalidEntryFormat } @@ -659,6 +664,7 @@ private func directIndexBoundComparator(lhs: (bound: Range return lhs.bound.sortOrder(comparedTo: indexedValue, order: lhs.order) } +@Sendable private func secondaryIndexBoundComparator(lhs: (bound: RangeBoundExpression, order: RangeOrder), rhs: DatastorePageEntry) throws -> SortOrder { guard rhs.headers.count == 1 else { throw DiskPersistenceError.invalidEntryFormat } @@ -673,9 +679,9 @@ private func secondaryIndexBoundComparator(lhs: (bound: Ra extension DiskPersistence.Transaction { func primaryIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, datastoreKey: DatastoreKey, - instanceConsumer: (_ versionData: Data, _ instanceData: Data) async throws -> () + instanceConsumer: @Sendable (_ versionData: Data, _ instanceData: Data) async throws -> () ) async throws { try checkIsActive() @@ -739,10 +745,10 @@ extension DiskPersistence.Transaction { } func directIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, indexName: IndexName, datastoreKey: DatastoreKey, - instanceConsumer: (_ versionData: Data, _ instanceData: Data) async throws -> () + instanceConsumer: @Sendable (_ versionData: Data, _ instanceData: Data) async throws -> () ) async throws { try checkIsActive() @@ -807,10 +813,10 @@ extension DiskPersistence.Transaction { } func secondaryIndexScan( - range: any IndexRangeExpression, + range: some IndexRangeExpression & Sendable, indexName: IndexName, datastoreKey: DatastoreKey, - identifierConsumer: (_ identifier: IdentifierType) async throws -> () + identifierConsumer: @Sendable (_ identifier: IdentifierType) async throws -> () ) async throws { try checkIsActive() @@ -1334,7 +1340,7 @@ extension DiskPersistence.Transaction { // MARK: - Helper Types -fileprivate protocol AnyDiskTransaction {} +fileprivate protocol AnyDiskTransaction: Sendable {} fileprivate enum TransactionTaskLocals { @TaskLocal diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift index 5e934db..e95b685 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/TypedIdentifier.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -16,7 +16,7 @@ struct TypedIdentifier: TypedIdentifierProtocol { } } -protocol TypedIdentifierProtocol: RawRepresentable, Codable, Equatable, Hashable, CustomStringConvertible, Comparable { +protocol TypedIdentifierProtocol: RawRepresentable, Codable, Equatable, Hashable, CustomStringConvertible, Comparable, Sendable { var rawValue: String { get } init(rawValue: String) } diff --git a/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift b/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift index 394b5a5..888a7ba 100644 --- a/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-03. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation @@ -13,10 +13,10 @@ public actor MemoryPersistence: Persistence { } extension MemoryPersistence { - public func _withTransaction( + public func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - transaction: @escaping (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + transaction: @Sendable (_ transaction: 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 45f5898..8b5b6bd 100644 --- a/Sources/CodableDatastore/Persistence/Persistence.swift +++ b/Sources/CodableDatastore/Persistence/Persistence.swift @@ -3,13 +3,13 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-03. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation /// A persistence used to group multiple data stores into a common store. -public protocol Persistence { +public protocol Persistence: Sendable { associatedtype AccessMode: _AccessMode /// Perform a transaction on the persistence with the specified options. @@ -20,7 +20,7 @@ public protocol Persistence { func _withTransaction( actionName: String?, options: UnsafeTransactionOptions, - transaction: @escaping @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T + @_inheritActorContext transaction: @Sendable (_ transaction: DatastoreInterfaceProtocol, _ isDurable: Bool) async throws -> T ) async throws -> T } @@ -40,7 +40,7 @@ extension Persistence { public func perform( actionName: String? = nil, options: TransactionOptions = [], - transaction: @escaping (_ persistence: Self, _ isDurable: Bool) async throws -> T + _inheritActorContext transaction: @Sendable (_ persistence: Self, _ isDurable: Bool) async throws -> T ) async throws -> T { try await _withTransaction( actionName: actionName, @@ -65,7 +65,7 @@ extension Persistence { public func perform( actionName: String? = nil, options: TransactionOptions = [], - transaction: @escaping () async throws -> T + @_inheritActorContext transaction: @Sendable () async throws -> T ) async throws -> T { try await _withTransaction( actionName: actionName, diff --git a/Sources/CodableDatastore/Persistence/TransactionOptions.swift b/Sources/CodableDatastore/Persistence/TransactionOptions.swift index 1e3a026..701c7da 100644 --- a/Sources/CodableDatastore/Persistence/TransactionOptions.swift +++ b/Sources/CodableDatastore/Persistence/TransactionOptions.swift @@ -3,13 +3,13 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-20. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import Foundation /// A set of options that the caller of a transaction can specify. -public struct TransactionOptions: OptionSet { +public struct TransactionOptions: OptionSet, Sendable { public let rawValue: UInt64 public init(rawValue: UInt64) { @@ -29,7 +29,7 @@ public struct TransactionOptions: OptionSet { /// A set of options that the caller of a transaction can specify. /// /// These options are generally unsafe to use improperly, and should generally not be used. -public struct UnsafeTransactionOptions: OptionSet { +public struct UnsafeTransactionOptions: OptionSet, Sendable { public let rawValue: UInt64 public init(rawValue: UInt64) { diff --git a/Tests/CodableDatastoreTests/DatastoreDescriptorTests.swift b/Tests/CodableDatastoreTests/DatastoreDescriptorTests.swift index 038767e..8328d20 100644 --- a/Tests/CodableDatastoreTests/DatastoreDescriptorTests.swift +++ b/Tests/CodableDatastoreTests/DatastoreDescriptorTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-11. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest @@ -56,8 +56,8 @@ final class DatastoreDescriptorTests: XCTestCase { } struct SampleFormatA: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -83,8 +83,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatB: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -111,8 +111,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatC: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -141,8 +141,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatD: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -192,8 +192,8 @@ final class DatastoreDescriptorTests: XCTestCase { } struct SampleFormatA: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -217,8 +217,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatB: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -243,8 +243,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatC: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -271,8 +271,8 @@ final class DatastoreDescriptorTests: XCTestCase { ]) struct SampleFormatD: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType @@ -320,8 +320,8 @@ final class DatastoreDescriptorTests: XCTestCase { } struct SampleFormatA: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = SharedVersion.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = SharedVersion.a typealias Version = SharedVersion typealias Instance = SampleType diff --git a/Tests/CodableDatastoreTests/DatastoreFormatTests.swift b/Tests/CodableDatastoreTests/DatastoreFormatTests.swift index a503d8a..4e2ee9f 100644 --- a/Tests/CodableDatastoreTests/DatastoreFormatTests.swift +++ b/Tests/CodableDatastoreTests/DatastoreFormatTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2024-04-12. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest @@ -14,8 +14,8 @@ final class DatastoreFormatTests: XCTestCase { struct NonCodable {} struct TestFormat: DatastoreFormat { - static var defaultKey = DatastoreKey("sample") - static var currentVersion = Version.a + static let defaultKey = DatastoreKey("sample") + static let currentVersion = Version.a enum Version: String, CaseIterable { case a, b, c diff --git a/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift b/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift index a2a6589..8589112 100644 --- a/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift +++ b/Tests/CodableDatastoreTests/DatastorePageEntryTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-04. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest diff --git a/Tests/CodableDatastoreTests/DatedIdentifierTests.swift b/Tests/CodableDatastoreTests/DatedIdentifierTests.swift index 2ebecd2..07f0bfb 100644 --- a/Tests/CodableDatastoreTests/DatedIdentifierTests.swift +++ b/Tests/CodableDatastoreTests/DatedIdentifierTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest diff --git a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift index effb454..264e1cc 100644 --- a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift +++ b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift @@ -3,9 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-04. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if !canImport(Darwin) +@preconcurrency import Foundation +#endif import XCTest @testable import CodableDatastore @@ -64,7 +67,7 @@ final class DiskPersistenceDatastoreIndexTests: XCTestCase { pageInfos.append(.existing(pageID)) } - let result = try await index.pageIndex(for: proposedEntry, in: pageInfos) { pageID in + let result = try await index.pageIndex(for: proposedEntry, in: pageInfos) { [pageLookup] pageID in pageLookup[pageID]! } comparator: { lhs, rhs in lhs.sortOrder(comparedTo: rhs.headers[0][0]) @@ -118,7 +121,7 @@ final class DiskPersistenceDatastoreIndexTests: XCTestCase { pageInfos.append(.existing(pageID)) } - let result = try await index.insertionCursor(for: RangeBoundExpression.including(proposedEntry), in: pageInfos) { pageID in + let result = try await index.insertionCursor(for: RangeBoundExpression.including(proposedEntry), in: pageInfos) { [pageLookup] pageID in pageLookup[pageID]! } comparator: { lhs, rhs in lhs.sortOrder(comparedTo: rhs.headers[0][0], order: .ascending) diff --git a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift index 2b57908..c7edac4 100644 --- a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift +++ b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift @@ -3,9 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-02. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if !canImport(Darwin) +@preconcurrency import Foundation +#endif import XCTest @testable import CodableDatastore diff --git a/Tests/CodableDatastoreTests/DiskPersistenceTests.swift b/Tests/CodableDatastoreTests/DiskPersistenceTests.swift index a8f5bc6..c5934ec 100644 --- a/Tests/CodableDatastoreTests/DiskPersistenceTests.swift +++ b/Tests/CodableDatastoreTests/DiskPersistenceTests.swift @@ -3,9 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-07. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if !canImport(Darwin) +@preconcurrency import Foundation +#endif import XCTest @testable import CodableDatastore diff --git a/Tests/CodableDatastoreTests/DiskTransactionTests.swift b/Tests/CodableDatastoreTests/DiskTransactionTests.swift index 30fa1b4..c8dd1d6 100644 --- a/Tests/CodableDatastoreTests/DiskTransactionTests.swift +++ b/Tests/CodableDatastoreTests/DiskTransactionTests.swift @@ -3,9 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-02. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if !canImport(Darwin) +@preconcurrency import Foundation +#endif import XCTest @testable import CodableDatastore diff --git a/Tests/CodableDatastoreTests/IndexRangeExpressionTests.swift b/Tests/CodableDatastoreTests/IndexRangeExpressionTests.swift index 7019436..c066932 100644 --- a/Tests/CodableDatastoreTests/IndexRangeExpressionTests.swift +++ b/Tests/CodableDatastoreTests/IndexRangeExpressionTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-05. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest diff --git a/Tests/CodableDatastoreTests/SnapshotTests.swift b/Tests/CodableDatastoreTests/SnapshotTests.swift index e14633f..56a7a75 100644 --- a/Tests/CodableDatastoreTests/SnapshotTests.swift +++ b/Tests/CodableDatastoreTests/SnapshotTests.swift @@ -3,9 +3,12 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // +#if !canImport(Darwin) +@preconcurrency import Foundation +#endif import XCTest @testable import CodableDatastore diff --git a/Tests/CodableDatastoreTests/TransactionOptionsTests.swift b/Tests/CodableDatastoreTests/TransactionOptionsTests.swift index 4f2fb88..a7dd9cd 100644 --- a/Tests/CodableDatastoreTests/TransactionOptionsTests.swift +++ b/Tests/CodableDatastoreTests/TransactionOptionsTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-07-12. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest diff --git a/Tests/CodableDatastoreTests/TypedIdentifierTests.swift b/Tests/CodableDatastoreTests/TypedIdentifierTests.swift index bcfeac0..35b3a34 100644 --- a/Tests/CodableDatastoreTests/TypedIdentifierTests.swift +++ b/Tests/CodableDatastoreTests/TypedIdentifierTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-10. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest diff --git a/Tests/CodableDatastoreTests/UUIDTests.swift b/Tests/CodableDatastoreTests/UUIDTests.swift index c786a82..7ab7b42 100644 --- a/Tests/CodableDatastoreTests/UUIDTests.swift +++ b/Tests/CodableDatastoreTests/UUIDTests.swift @@ -3,7 +3,7 @@ // CodableDatastore // // Created by Dimitri Bouniol on 2023-06-04. -// Copyright © 2023 Mochi Development, Inc. All rights reserved. +// Copyright © 2023-24 Mochi Development, Inc. All rights reserved. // import XCTest