From 4886402e25541534f5c427a7b9b3337c4e494afa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 25 Aug 2026 13:27:27 -0700 Subject: [PATCH 1/2] ID `@Fetch*.load` events to fix cancel race --- Sources/SQLiteData/Fetch.swift | 30 +++- Sources/SQLiteData/FetchAll+Sections.swift | 72 ++++++---- Sources/SQLiteData/FetchAll.swift | 56 +++++--- Sources/SQLiteData/FetchOne.swift | 135 ++++++++++-------- Sources/SQLiteData/FetchSubscription.swift | 19 ++- Sources/SQLiteData/Internal/FetchBox.swift | 4 + .../SQLiteData/Internal/LoadGeneration.swift | 48 +++++++ .../FetchSubscriptionTests.swift | 18 +++ 8 files changed, 267 insertions(+), 115 deletions(-) create mode 100644 Sources/SQLiteData/Internal/LoadGeneration.swift diff --git a/Sources/SQLiteData/Fetch.swift b/Sources/SQLiteData/Fetch.swift index ee36aa4a..ed50771d 100644 --- a/Sources/SQLiteData/Fetch.swift +++ b/Sources/SQLiteData/Fetch.swift @@ -39,12 +39,16 @@ public struct Fetch: Sendable { private let box: FetchBox private let state: SwiftUI.State> private let generation = SwiftUI.State(wrappedValue: 0) + + var loadGeneration: LoadGeneration { state.wrappedValue.loadGeneration } #else /// The underlying shared reader powering the property wrapper. /// /// Shared readers come from the [Sharing](https://github.com/pointfreeco/swift-sharing) /// package, a general solution to observing and persisting changes to external data sources. public let sharedReader: SharedReader + + let loadGeneration = LoadGeneration() #endif /// Data associated with the underlying query. @@ -58,7 +62,10 @@ public struct Fetch: Sendable { /// ``isLoading``, and ``publisher``. public var projectedValue: Self { get { self } - nonmutating set { sharedReader.projectedValue = newValue.sharedReader.projectedValue } + nonmutating set { + loadGeneration.invalidate() + sharedReader.projectedValue = newValue.sharedReader.projectedValue + } } /// Returns a ``sharedReader`` for the given key path. @@ -127,8 +134,15 @@ public struct Fetch: Sendable { _ request: some FetchKeyRequest, database: (any DatabaseReader)? = nil ) async throws -> FetchSubscription { - try await sharedReader.load(.fetch(request, database: database)) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load(.fetch(request, database: database)) + } + } + + private func withSubscription(_ load: () async throws -> Void) async throws -> FetchSubscription { + let token = loadGeneration.begin() + try await load() + return FetchSubscription(sharedReader: sharedReader, token: token) } #if !canImport(SwiftUI) @@ -183,8 +197,9 @@ extension Fetch { database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) async throws -> FetchSubscription { - try await sharedReader.load(.fetch(request, database: database, scheduler: scheduler)) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load(.fetch(request, database: database, scheduler: scheduler)) + } } } @@ -249,8 +264,9 @@ extension Fetch: Equatable where Value: Equatable { database: (any DatabaseReader)? = nil, animation: Animation? ) async throws -> FetchSubscription { - try await sharedReader.load(.fetch(request, database: database, animation: animation)) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load(.fetch(request, database: database, animation: animation)) + } } } #endif diff --git a/Sources/SQLiteData/FetchAll+Sections.swift b/Sources/SQLiteData/FetchAll+Sections.swift index 0a822950..16f9b3b1 100644 --- a/Sources/SQLiteData/FetchAll+Sections.swift +++ b/Sources/SQLiteData/FetchAll+Sections.swift @@ -196,7 +196,8 @@ extension FetchAll { /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -232,7 +233,9 @@ extension FetchAll { /// (`@Dependency(\.defaultDatabase)`). @_documentation(visibility: private) public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -371,7 +374,8 @@ extension FetchAll { /// - Returns: A subscription associated with the observation. @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, @@ -404,7 +408,9 @@ extension FetchAll { @_documentation(visibility: private) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> @@ -522,14 +528,15 @@ extension FetchAll { guard let sectioning else { removeSections() let statement: Select = statement.selectStar() - try await sharedReader.load( - FetchKey( - request: FetchAllStatementValueRequest(statement: statement), - database: database, - scheduler: scheduler + return try await withSubscription { + try await sharedReader.load( + FetchKey( + request: FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } return try await loadSections( request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), @@ -553,14 +560,15 @@ extension FetchAll { defer { sharedReader.projectedValue = sectionedReader.elements.projectedValue } - try await sectionedReader.load( - FetchKey( - request: request, - database: database, - scheduler: scheduler + return try await withSubscription { + try await sectionedReader.load( + FetchKey( + request: request, + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader, sectionedReader: sectionedReader) + } } } @@ -714,7 +722,8 @@ extension FetchAll { /// - scheduler: The scheduler to observe from. By default, database observation is performed /// asynchronously on the main queue. public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -753,7 +762,9 @@ extension FetchAll { /// asynchronously on the main queue. @_documentation(visibility: private) public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -905,7 +916,8 @@ extension FetchAll { /// - Returns: A subscription associated with the observation. @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, @@ -941,7 +953,9 @@ extension FetchAll { @_documentation(visibility: private) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> @@ -1155,7 +1169,8 @@ extension FetchAll { /// the fetched results. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -1191,7 +1206,9 @@ extension FetchAll { @_documentation(visibility: private) @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], _ statement: Select, @@ -1340,7 +1357,8 @@ extension FetchAll { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, @@ -1374,7 +1392,9 @@ extension FetchAll { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J: StructuredQueriesCore.Table >( _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> diff --git a/Sources/SQLiteData/FetchAll.swift b/Sources/SQLiteData/FetchAll.swift index af10326a..9203e6f7 100644 --- a/Sources/SQLiteData/FetchAll.swift +++ b/Sources/SQLiteData/FetchAll.swift @@ -48,6 +48,8 @@ public struct FetchAll: Sendable { private let box: FetchAllBox private let state: SwiftUI.State> private let generation = SwiftUI.State(wrappedValue: 0) + + var loadGeneration: LoadGeneration { state.wrappedValue.loadGeneration } #else /// The underlying shared reader powering the property wrapper. /// @@ -59,6 +61,8 @@ public struct FetchAll: Sendable { SharedReader(value: ResultsSectionCollection()) let sectioning = LockIsolated<_Sectioning?>(nil) + + let loadGeneration = LoadGeneration() #endif /// A collection of data associated with the underlying query. @@ -73,6 +77,7 @@ public struct FetchAll: Sendable { public var projectedValue: Self { get { self } nonmutating set { + loadGeneration.invalidate() sharedReader.projectedValue = newValue.sharedReader.projectedValue sectionedReader.projectedValue = newValue.sectionedReader.projectedValue sectioning.setValue(newValue.sectioning.value) @@ -247,13 +252,22 @@ public struct FetchAll: Sendable { V.QueryOutput: Sendable { removeSections() - try await sharedReader.load( - .fetch( - FetchAllStatementValueRequest(statement: statement), - database: database + return try await withSubscription { + try await sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } + } + + func withSubscription(_ load: () async throws -> Void) async throws -> FetchSubscription { + let token = loadGeneration.begin() + try await load() + return sectioning.value == nil + ? FetchSubscription(sharedReader: sharedReader, token: token) + : FetchSubscription(sharedReader: sharedReader, sectionedReader: sectionedReader, token: token) } #if !canImport(SwiftUI) @@ -435,14 +449,15 @@ extension FetchAll { V.QueryOutput: Sendable { removeSections() - try await sharedReader.load( - .fetch( - FetchAllStatementValueRequest(statement: statement), - database: database, - scheduler: scheduler + return try await withSubscription { + try await sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } } @@ -635,14 +650,15 @@ extension FetchAll: Equatable where Element: Equatable { V.QueryOutput: Sendable { removeSections() - try await sharedReader.load( - .fetch( - FetchAllStatementValueRequest(statement: statement), - database: database, - animation: animation + return try await withSubscription { + try await sharedReader.load( + .fetch( + FetchAllStatementValueRequest(statement: statement), + database: database, + animation: animation + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } } #endif diff --git a/Sources/SQLiteData/FetchOne.swift b/Sources/SQLiteData/FetchOne.swift index 7bb4491c..37e4329c 100644 --- a/Sources/SQLiteData/FetchOne.swift +++ b/Sources/SQLiteData/FetchOne.swift @@ -39,12 +39,16 @@ public struct FetchOne: Sendable { private let box: FetchBox private let state: SwiftUI.State> private let generation = SwiftUI.State(wrappedValue: 0) + + var loadGeneration: LoadGeneration { state.wrappedValue.loadGeneration } #else /// The underlying shared reader powering the property wrapper. /// /// Shared readers come from the [Sharing](https://github.com/pointfreeco/swift-sharing) /// package, a general solution to observing and persisting changes to external data sources. public let sharedReader: SharedReader + + let loadGeneration = LoadGeneration() #endif /// A value associated with the underlying query. @@ -58,7 +62,10 @@ public struct FetchOne: Sendable { /// ``isLoading``, and ``publisher``. public var projectedValue: Self { get { self } - nonmutating set { sharedReader.projectedValue = newValue.sharedReader.projectedValue } + nonmutating set { + loadGeneration.invalidate() + sharedReader.projectedValue = newValue.sharedReader.projectedValue + } } /// Returns a ``sharedReader`` for the given key path. @@ -408,10 +415,11 @@ public struct FetchOne: Sendable { where Value == V.QueryOutput { - try await sharedReader.load( - .fetch(FetchOneStatementValueRequest(statement: statement), database: database) - ) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load( + .fetch(FetchOneStatementValueRequest(statement: statement), database: database) + ) + } } /// Replaces the wrapped value with data from the given query. @@ -429,10 +437,11 @@ public struct FetchOne: Sendable { where Value == V.QueryOutput? { - try await sharedReader.load( - .fetch(FetchOneStatementOptionalValueRequest(statement: statement), database: database) - ) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load( + .fetch(FetchOneStatementOptionalValueRequest(statement: statement), database: database) + ) + } } /// Replaces the wrapped value with data from the given query. @@ -454,10 +463,11 @@ public struct FetchOne: Sendable { S.Joins == () { let statement = statement.selectStar().asSelect().limit(1) - try await sharedReader.load( - .fetch(FetchOneStatementOptionalValueRequest(statement: statement), database: database) - ) - return FetchSubscription(sharedReader: sharedReader) + return try await withSubscription { + try await sharedReader.load( + .fetch(FetchOneStatementOptionalValueRequest(statement: statement), database: database) + ) + } } /// Replaces the wrapped value with data from the given query. @@ -478,10 +488,11 @@ public struct FetchOne: Sendable { S.QueryValue: StructuredQueriesCore._OptionalProtocol, Value == S.QueryValue.QueryOutput { - try await sharedReader.load( - .fetch(FetchOneStatementOptionalProtocolRequest(statement: statement), database: database) - ) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load( + .fetch(FetchOneStatementOptionalProtocolRequest(statement: statement), database: database) + ) + } } /// Replaces the wrapped value with data from the given query. @@ -501,10 +512,17 @@ public struct FetchOne: Sendable { Value: StructuredQueriesCore._OptionalProtocol, Value.QueryOutput == Value { - try await sharedReader.load( - .fetch(FetchOneStatementOptionalProtocolRequest(statement: statement), database: database) - ) - return FetchSubscription(sharedReader: sharedReader) + try await withSubscription { + try await sharedReader.load( + .fetch(FetchOneStatementOptionalProtocolRequest(statement: statement), database: database) + ) + } + } + + private func withSubscription(_ load: () async throws -> Void) async throws -> FetchSubscription { + let token = loadGeneration.begin() + try await load() + return FetchSubscription(sharedReader: sharedReader, token: token) } #if !canImport(SwiftUI) @@ -918,14 +936,15 @@ extension FetchOne { where Value == V.QueryOutput { - try await sharedReader.load( - .fetch( - FetchOneStatementValueRequest(statement: statement), - database: database, - scheduler: scheduler + try await withSubscription { + try await sharedReader.load( + .fetch( + FetchOneStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } /// Replaces the wrapped value with data from the given query. @@ -946,14 +965,15 @@ extension FetchOne { where Value == V.QueryOutput? { - try await sharedReader.load( - .fetch( - FetchOneStatementOptionalValueRequest(statement: statement), - database: database, - scheduler: scheduler + try await withSubscription { + try await sharedReader.load( + .fetch( + FetchOneStatementOptionalValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } /// Replaces the wrapped value with data from the given query. @@ -978,14 +998,15 @@ extension FetchOne { S.Joins == () { let statement = statement.selectStar().asSelect().limit(1) - try await sharedReader.load( - .fetch( - FetchOneStatementOptionalValueRequest(statement: statement), - database: database, - scheduler: scheduler + return try await withSubscription { + try await sharedReader.load( + .fetch( + FetchOneStatementOptionalValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } /// Replaces the wrapped value with data from the given query. @@ -1009,14 +1030,15 @@ extension FetchOne { S.QueryValue: StructuredQueriesCore._OptionalProtocol, Value == S.QueryValue.QueryOutput { - try await sharedReader.load( - .fetch( - FetchOneStatementOptionalProtocolRequest(statement: statement), - database: database, - scheduler: scheduler + try await withSubscription { + try await sharedReader.load( + .fetch( + FetchOneStatementOptionalProtocolRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } /// Replaces the wrapped value with data from the given query. @@ -1039,14 +1061,15 @@ extension FetchOne { Value: StructuredQueriesCore._OptionalProtocol, Value.QueryOutput == Value { - try await sharedReader.load( - .fetch( - FetchOneStatementOptionalProtocolRequest(statement: statement), - database: database, - scheduler: scheduler + try await withSubscription { + try await sharedReader.load( + .fetch( + FetchOneStatementOptionalProtocolRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) - ) - return FetchSubscription(sharedReader: sharedReader) + } } } diff --git a/Sources/SQLiteData/FetchSubscription.swift b/Sources/SQLiteData/FetchSubscription.swift index bfbaf575..ff87335c 100644 --- a/Sources/SQLiteData/FetchSubscription.swift +++ b/Sources/SQLiteData/FetchSubscription.swift @@ -18,18 +18,25 @@ public struct FetchSubscription: Sendable { let cancellable = LockIsolated?>(nil) let onCancel: @Sendable () -> Void - init(sharedReader: SharedReader) { - onCancel = { sharedReader.projectedValue = SharedReader(value: sharedReader.wrappedValue) } + init(sharedReader: SharedReader, token: LoadGeneration.Token) { + onCancel = { + token.ifCurrent { + sharedReader.projectedValue = SharedReader(value: sharedReader.wrappedValue) + } + } } init( sharedReader: SharedReader<[Element]>, - sectionedReader: SharedReader> + sectionedReader: SharedReader>, + token: LoadGeneration.Token ) { onCancel = { - let sections = sectionedReader.wrappedValue - sectionedReader.projectedValue = SharedReader(value: sections) - sharedReader.projectedValue = SharedReader(value: sections.elements) + token.ifCurrent { + let sections = sectionedReader.wrappedValue + sectionedReader.projectedValue = SharedReader(value: sections) + sharedReader.projectedValue = SharedReader(value: sections.elements) + } } } diff --git a/Sources/SQLiteData/Internal/FetchBox.swift b/Sources/SQLiteData/Internal/FetchBox.swift index ed21e1c6..2ca0ff8e 100644 --- a/Sources/SQLiteData/Internal/FetchBox.swift +++ b/Sources/SQLiteData/Internal/FetchBox.swift @@ -6,6 +6,7 @@ final class FetchBox: Sendable { let sharedReader: SharedReader + let loadGeneration = LoadGeneration() private let storage = LockIsolated(Storage()) var fetchKeyID: FetchKeyID? { @@ -25,6 +26,7 @@ return true } guard isAdopted else { return } + loadGeneration.invalidate() sharedReader.projectedValue = other.sharedReader.projectedValue } @@ -48,6 +50,7 @@ let sharedReader: SharedReader<[Element]> let sectionedReader: SharedReader> let sectioning = LockIsolated<_Sectioning?>(nil) + let loadGeneration = LoadGeneration() private let storage = LockIsolated(Storage()) var fetchKeyID: FetchKeyID? { @@ -68,6 +71,7 @@ return true } guard isAdopted else { return } + loadGeneration.invalidate() sharedReader.projectedValue = other.sharedReader.projectedValue sectionedReader.projectedValue = other.sectionedReader.projectedValue sectioning.setValue(other.sectioning.value) diff --git a/Sources/SQLiteData/Internal/LoadGeneration.swift b/Sources/SQLiteData/Internal/LoadGeneration.swift new file mode 100644 index 00000000..540b819e --- /dev/null +++ b/Sources/SQLiteData/Internal/LoadGeneration.swift @@ -0,0 +1,48 @@ +import Foundation + +final class LoadGeneration: Sendable { + private let count = LockIsolated(0) + + func begin() -> Token { + Token( + count: count, + generation: count.withLock { + $0 += 1 + return $0 + } + ) + } + + func invalidate() { + count.withLock { $0 += 1 } + } + + struct Token: Sendable { + fileprivate let count: LockIsolated + fileprivate let generation: Int + + func ifCurrent(_ body: sending () -> Void) { + count.withLock { + guard $0 == generation else { return } + body() + } + } + } +} + +private final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSLock() + init(_ value: sending Value) { + self._value = value + } + func withLock( + _ operation: sending (inout sending Value) throws -> sending T + ) rethrows -> sending T { + lock.lock() + defer { lock.unlock() } + var value = _value + defer { _value = value } + return try operation(&value) + } +} diff --git a/Tests/SQLiteDataTests/FetchSubscriptionTests.swift b/Tests/SQLiteDataTests/FetchSubscriptionTests.swift index 155dcdc2..b5798013 100644 --- a/Tests/SQLiteDataTests/FetchSubscriptionTests.swift +++ b/Tests/SQLiteDataTests/FetchSubscriptionTests.swift @@ -53,6 +53,24 @@ import Testing #expect(didComplete.value) } + @Test func staleCancellationDoesNotStopNewerLoad() async throws { + @FetchAll var records: [Record] + + let firstSubscription = try await $records.load(Record.all) + let task = Task { + try? await firstSubscription.task + } + try await $records.load(Record.where { $0.id > 0 }) + task.cancel() + await task.value + + try await database.write { db in + try Record.insert { Record.Draft() }.execute(db) + } + try await $records.load() + #expect(records.count == 1) + } + @Test func cancellingOneFetchDoesNotCancelAnother() async throws { @FetchAll var records1: [Record] #expect(records1.count == 0) From f7885eac721d532d2e5d773864f6575aa80a3fd8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 25 Aug 2026 14:31:58 -0700 Subject: [PATCH 2/2] Fix --- .../SQLiteData/Internal/LoadGeneration.swift | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/Sources/SQLiteData/Internal/LoadGeneration.swift b/Sources/SQLiteData/Internal/LoadGeneration.swift index 540b819e..e4e3a56e 100644 --- a/Sources/SQLiteData/Internal/LoadGeneration.swift +++ b/Sources/SQLiteData/Internal/LoadGeneration.swift @@ -1,48 +1,31 @@ import Foundation -final class LoadGeneration: Sendable { - private let count = LockIsolated(0) +final class LoadGeneration: @unchecked Sendable { + private let lock = NSLock() + private var count = 0 func begin() -> Token { - Token( - count: count, - generation: count.withLock { - $0 += 1 - return $0 - } - ) + lock.lock() + defer { lock.unlock() } + count += 1 + return Token(loadGeneration: self, generation: count) } func invalidate() { - count.withLock { $0 += 1 } + lock.lock() + defer { lock.unlock() } + count += 1 } struct Token: Sendable { - fileprivate let count: LockIsolated + fileprivate let loadGeneration: LoadGeneration fileprivate let generation: Int - func ifCurrent(_ body: sending () -> Void) { - count.withLock { - guard $0 == generation else { return } - body() - } + func ifCurrent(_ body: () -> Void) { + loadGeneration.lock.lock() + defer { loadGeneration.lock.unlock() } + guard loadGeneration.count == generation else { return } + body() } } } - -private final class LockIsolated: @unchecked Sendable { - private var _value: Value - private let lock = NSLock() - init(_ value: sending Value) { - self._value = value - } - func withLock( - _ operation: sending (inout sending Value) throws -> sending T - ) rethrows -> sending T { - lock.lock() - defer { lock.unlock() } - var value = _value - defer { _value = value } - return try operation(&value) - } -}