From a828f453c29f415fc2159f8161e4c5e78f3f68fc Mon Sep 17 00:00:00 2001 From: DomasZ Date: Wed, 5 Aug 2026 11:26:37 +0300 Subject: [PATCH] Make cachedValues non lazy, strong capture model --- Sources/WrapModel/WrapModel.swift | 4 +++- Tests/ThreadSafetyTests.swift | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Sources/WrapModel/WrapModel.swift b/Sources/WrapModel/WrapModel.swift index f155218..4da11fc 100644 --- a/Sources/WrapModel/WrapModel.swift +++ b/Sources/WrapModel/WrapModel.swift @@ -170,7 +170,7 @@ open class WrapModel : NSObject, NSCopying, NSMutableCopying, NSSecureCoding { private var contributedLock:WrapModelLock? private var cacheLock = WrapModelLock() private var cacheLockLock = WrapModelLock() - private lazy var cachedValues = [String:Any]() + private var cachedValues = [String:Any]() fileprivate func getCached(forProperty property:AnyWrapProperty) -> Any? { return lock.reading { return self.cachedValues[property.keyPath] @@ -444,6 +444,7 @@ open class WrapProperty : AnyWrapProperty { } private func internalValue() -> T? { + guard let model = self.model else { return nil } if let cachedValue = model.getCached(forProperty: self) { if cachedValue is NSNull { return nil } return cachedValue as? T @@ -461,6 +462,7 @@ open class WrapProperty : AnyWrapProperty { } private func internalSetValue(_ value: T) { + guard let model = self.model else { return } assert(model.isMutable, "Attempt to mutate immutable model") guard model.isMutable else { return } switch value { diff --git a/Tests/ThreadSafetyTests.swift b/Tests/ThreadSafetyTests.swift index 7cc9ede..949481e 100644 --- a/Tests/ThreadSafetyTests.swift +++ b/Tests/ThreadSafetyTests.swift @@ -183,6 +183,35 @@ final class ThreadSafetyTests: XCTestCase { group.wait() } + // MARK: - Test 6: Concurrent property .value reads on a SHARED instance + + /// Races the property `.value` getter path (internalValue -> getCached/setCached) on ONE + /// shared model across many threads. Each property's first read populates `cachedValues`; + /// on master that lazy first-touch is a write on the concurrent read path -> data race. + /// Must be clean after making `cachedValues` non-lazy. This is the browse/Follows crash path. + func testConcurrentPropertyValueReadsSharedInstance() { + let iterations = 1000 + let threads = 16 + let group = DispatchGroup() + + for _ in 0..