Fixed QA issues

• Refactored the 'performOperation' function to have cleaner control flow and resolve another EXC_BAD_ACCESS edge case
• Updated the code to allow legacy groups to be unpinned after they are deprecated
• Fixed an issue where the default state of the global search screen wouldn't be populated if you had a contact with no SessionThread record
• Fixed an issue with display picture placeholder generation
• Fixed an issue where the edit group screen would show the group display picture back to front
pull/894/head
Morgan Pretty 2 months ago
parent 70ef1c451c
commit 1f3f7ba7c6

@ -408,7 +408,6 @@
FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; };
FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; };
FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; };
FD0606BF2BC8C10200C3816E /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */; };
FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; };
FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; };
FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; };
@ -1689,7 +1688,6 @@
FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = "<group>"; }; FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = "<group>"; };
FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = "<group>"; }; FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = "<group>"; };
FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = "<group>"; }; FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = "<group>"; };
FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = "<group>"; };
FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = "<group>"; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = "<group>"; };
FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = "<group>"; }; FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = "<group>"; };
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; }; FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
@ -3886,7 +3884,6 @@
FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */, FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */,
FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */, FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */,
FD1936462ACE752F004BCF0F /* _005_AddJobUniqueHash.swift */, FD1936462ACE752F004BCF0F /* _005_AddJobUniqueHash.swift */,
FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */,
); );
path = Migrations; path = Migrations;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5977,7 +5974,6 @@
FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */,
FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */,
943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */, 943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */,
FD0606BF2BC8C10200C3816E /* _005_AddJobUniqueHash.swift in Sources */,
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */,
FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */,
FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */, FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */,
@ -7935,7 +7931,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 541; CURRENT_PROJECT_VERSION = 542;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -8011,7 +8007,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 541; CURRENT_PROJECT_VERSION = 542;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_MODULE_VERIFIER = YES; ENABLE_MODULE_VERIFIER = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;

@ -77,15 +77,15 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
private struct State: Equatable { private struct State: Equatable {
let group: ClosedGroup let group: ClosedGroup
let profileFront: Profile? let profile: Profile?
let profileBack: Profile? let additionalProfile: Profile?
let members: [WithProfile<GroupMember>] let members: [WithProfile<GroupMember>]
let isValid: Bool let isValid: Bool
static let invalidState: State = State( static let invalidState: State = State(
group: ClosedGroup(threadId: "", name: "", formationTimestamp: 0, shouldPoll: false, invited: false), group: ClosedGroup(threadId: "", name: "", formationTimestamp: 0, shouldPoll: false, invited: false),
profileFront: nil, profile: nil,
profileBack: nil, additionalProfile: nil,
members: [], members: [],
isValid: false isValid: false
) )
@ -133,8 +133,8 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
return State( return State(
group: group, group: group,
profileFront: profileFront, profile: profileBack,
profileBack: profileBack, additionalProfile: profileFront,
members: try GroupMember members: try GroupMember
.filter(GroupMember.Columns.groupId == threadId) .filter(GroupMember.Columns.groupId == threadId)
.fetchAllWithProfiles(db, using: dependencies), .fetchAllWithProfiles(db, using: dependencies),
@ -194,9 +194,9 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
size: .hero, size: .hero,
threadVariant: (isUpdatedGroup ? .group : .legacyGroup), threadVariant: (isUpdatedGroup ? .group : .legacyGroup),
displayPictureFilename: state.group.displayPictureFilename, displayPictureFilename: state.group.displayPictureFilename,
profile: state.profileFront, profile: state.profile,
profileIcon: .none, profileIcon: .none,
additionalProfile: state.profileBack, additionalProfile: state.additionalProfile,
additionalProfileIcon: .none, additionalProfileIcon: .none,
accessibility: nil accessibility: nil
), ),

@ -835,7 +835,13 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
// Cannot properly sync outgoing blinded message requests so only provide valid options // Cannot properly sync outgoing blinded message requests so only provide valid options
let shouldHavePinAction: Bool = { let shouldHavePinAction: Bool = {
switch threadViewModel.threadVariant { switch threadViewModel.threadVariant {
case .legacyGroup: return !viewModel.dependencies[feature: .legacyGroupsDeprecated] case .legacyGroup:
// Only allow unpin once deprecated
return (
!viewModel.dependencies[feature: .legacyGroupsDeprecated] ||
threadViewModel.threadPinnedPriority > 0
)
default: default:
return ( return (
sessionIdPrefix != .blinded15 && sessionIdPrefix != .blinded15 &&

@ -39,7 +39,7 @@ public extension Date {
} }
var formattedForBanner: String { var formattedForBanner: String {
return Date.dateOnlyFormatter.string(from: self) return Date.localTimeAndDateFormatter.string(from: self)
} }
} }
@ -96,6 +96,16 @@ fileprivate extension Date {
return result return result
}() }()
static let localTimeAndDateFormatter: DateFormatter = {
let result: DateFormatter = DateFormatter()
result.locale = Locale.current
// 2:12pm 6 Jun 2023
result.dateFormat = "h:mm a, d MMM YYYY"
return result
}()
static var hourFormat: String { static var hourFormat: String {
guard guard
let format: String = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current), let format: String = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current),

@ -2075,7 +2075,7 @@ public extension SessionThreadViewModel {
IFNULL(\(thread[.creationDateTimestamp]), \(currentTimestamp)) AS \(ViewModel.Columns.threadCreationDateTimestamp), IFNULL(\(thread[.creationDateTimestamp]), \(currentTimestamp)) AS \(ViewModel.Columns.threadCreationDateTimestamp),
'' AS \(ViewModel.Columns.threadMemberNames), '' AS \(ViewModel.Columns.threadMemberNames),
(\(SQL("\(thread[.id]) = \(userSessionId.hexString)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), (\(SQL("\(contact[.id]) = \(userSessionId.hexString)"))) AS \(ViewModel.Columns.threadIsNoteToSelf),
IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority),
\(contactProfile.allColumns), \(contactProfile.allColumns),

@ -4,20 +4,18 @@ import UIKit
import CryptoKit import CryptoKit
public class PlaceholderIcon { public class PlaceholderIcon {
private let seed: Int private static let colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color }
// Color palette private let seed: Int
private var colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color }
// MARK: - Initialization // MARK: - Initialization
init(seed: Int, colors: [UIColor]? = nil) { init(seed: Int) {
self.seed = seed self.seed = seed
if let colors = colors { self.colors = colors }
} }
// stringlint:ignore_contents // stringlint:ignore_contents
convenience init(seed: String, colors: [UIColor]? = nil) { convenience init(seed: String) {
// Ensure we have a correct hash // Ensure we have a correct hash
var hash = seed var hash = seed
@ -28,11 +26,11 @@ public class PlaceholderIcon {
} }
guard let number = Int(String(hash.prefix(12)), radix: 16) else { guard let number = Int(String(hash.prefix(12)), radix: 16) else {
self.init(seed: 0, colors: colors) self.init(seed: 0)
return return
} }
self.init(seed: number, colors: colors) self.init(seed: number)
} }
// MARK: - Convenience // MARK: - Convenience
@ -62,7 +60,7 @@ public class PlaceholderIcon {
.compactMap { word in word.first.map { String($0) } } .compactMap { word in word.first.map { String($0) } }
.joined() .joined()
return SNUIKit.placeholderIconCacher(cacheKey: "\(content)-\(Int(floor(size)))") { return SNUIKit.placeholderIconCacher(cacheKey: "\(seed)-\(Int(floor(size)))") {
let layer = icon.generateLayer( let layer = icon.generateLayer(
with: size, with: size,
text: (initials.count >= 2 ? text: (initials.count >= 2 ?
@ -81,7 +79,7 @@ public class PlaceholderIcon {
// MARK: - Internal // MARK: - Internal
private func generateLayer(with diameter: CGFloat, text: String) -> CALayer { private func generateLayer(with diameter: CGFloat, text: String) -> CALayer {
let color: UIColor = self.colors[seed % self.colors.count] let color: UIColor = PlaceholderIcon.colors[seed % PlaceholderIcon.colors.count]
let base: CALayer = getTextLayer(with: diameter, color: color, text: text) let base: CALayer = getTextLayer(with: diameter, color: color, text: text)
base.masksToBounds = true base.masksToBounds = true

@ -668,37 +668,40 @@ open class Storage {
/// ///
/// The `async` variants don't need to worry about this reentrancy issue so instead we route we use those for all operations instead /// The `async` variants don't need to worry about this reentrancy issue so instead we route we use those for all operations instead
/// and just block the thread when we want to perform a synchronous operation /// and just block the thread when we want to perform a synchronous operation
///
/// **Note:** When running a synchronous operation the result will be returned and `asyncCompletion` will not be called, and
/// vice-versa for an asynchronous operation
@discardableResult private static func performOperation<T>( @discardableResult private static func performOperation<T>(
_ info: CallInfo, _ info: CallInfo,
_ dependencies: Dependencies, _ dependencies: Dependencies,
_ operation: @escaping (Database) throws -> T, _ operation: @escaping (Database) throws -> T,
_ completion: ((Result<T, Error>) -> Void)? = nil _ asyncCompletion: ((Result<T, Error>) -> Void)? = nil
) -> Result<T, Error> { ) -> Result<T, Error> {
// A serial queue for synchronizing completion updates. // A serial queue for synchronizing completion updates.
let syncQueue = DispatchQueue(label: "com.session.performOperation.syncQueue") let syncQueue = DispatchQueue(label: "com.session.performOperation.syncQueue")
var queryDb: Database? var queryDb: Database?
var didComplete: Bool = false var didTimeout: Bool = false
var finalResult: Result<T, Error> = .failure(StorageError.invalidQueryResult) var operationResult: Result<T, Error>?
let semaphore: DispatchSemaphore? = (info.isAsync ? nil : DispatchSemaphore(value: 0)) let semaphore: DispatchSemaphore? = (info.isAsync ? nil : DispatchSemaphore(value: 0))
let logErrorIfNeeded: (Result<T, Error>) -> () = { result in let logErrorIfNeeded: (Result<T, Error>) -> Result<T, Error> = { result in
switch result { switch result {
case .success: break case .success: break
case .failure(let error): StorageState.logIfNeeded(error, isWrite: info.isWrite) case .failure(let error): StorageState.logIfNeeded(error, isWrite: info.isWrite)
} }
return result
} }
func completeOperation(with result: Result<T, Error>) { func completeOperation(with result: Result<T, Error>) {
syncQueue.sync { syncQueue.sync {
if didComplete { return } guard !didTimeout && operationResult == nil else { return }
didComplete = true operationResult = result
finalResult = result
semaphore?.signal() semaphore?.signal()
// For async operations, log and invoke the completion closure. // For async operations, log and invoke the completion closure.
if info.isAsync { if info.isAsync {
logErrorIfNeeded(result) asyncCompletion?(logErrorIfNeeded(result))
completion?(result)
} }
} }
} }
@ -749,63 +752,38 @@ open class Storage {
/// ///
/// To try to avoid this we have the below code to try to replicate the behaviour of the proper semaphore timeout while the debugger /// To try to avoid this we have the below code to try to replicate the behaviour of the proper semaphore timeout while the debugger
/// is attached as this approach does seem to get paused (or at least only perform a single iteration per debugger step) /// is attached as this approach does seem to get paused (or at least only perform a single iteration per debugger step)
var semaphoreResult: DispatchTimeoutResult? if let semaphore: DispatchSemaphore = semaphore {
var semaphoreResult: DispatchTimeoutResult
#if DEBUG
if !isDebuggerAttached() {
semaphoreResult = semaphore?.wait(timeout: .now() + .seconds(Storage.transactionDeadlockTimeoutSeconds))
}
else if !info.isAsync, let semaphore: DispatchSemaphore = semaphore {
let pollQueue: DispatchQueue = DispatchQueue.global(qos: .userInitiated)
let standardPollInterval: DispatchTimeInterval = .milliseconds(100)
var iterations: Int = 0
let maxIterations: Int = ((Storage.transactionDeadlockTimeoutSeconds * 1000) / standardPollInterval.milliseconds)
let pollCompletionSemaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
/// Stagger the size of the `pollIntervals` to avoid holding up the thread in case the query resolves very quickly (this #if DEBUG
/// means the timeout will occur ~500ms early but helps prevent false main thread lag appearing when debugging that wouldn't if isDebuggerAttached() {
/// affect production) semaphoreResult = debugWait(semaphore: semaphore, info: info)
let pollIntervals: [DispatchTimeInterval] = [ }
.milliseconds(5), .milliseconds(5), .milliseconds(10), .milliseconds(10), .milliseconds(10), else {
standardPollInterval semaphoreResult = semaphore.wait(timeout: .now() + .seconds(Storage.transactionDeadlockTimeoutSeconds))
] }
#else
semaphoreResult = semaphore?.wait(timeout: .now() + .seconds(Storage.transactionDeadlockTimeoutSeconds))
#endif
func pollSemaphore() { /// If the query timed out then we should interrupt the query (don't want the query thread to remain blocked when we've
iterations += 1 /// already handled it as a failure)
if semaphoreResult == .timedOut {
guard iterations < maxIterations && semaphore.wait(timeout: .now()) != .success else { syncQueue.sync {
pollCompletionSemaphore.signal() didTimeout = true
return queryDb?.interrupt()
}
let nextInterval: DispatchTimeInterval = pollIntervals[min(iterations, pollIntervals.count - 1)]
pollQueue.asyncAfter(deadline: .now() + nextInterval) {
pollSemaphore()
} }
} }
/// Poll the semaphore in a background queue /// Before returning we need to wait for any pending updates on `syncQueue` to be completed to ensure that objects
pollQueue.asyncAfter(deadline: .now() + pollIntervals[0]) { pollSemaphore() } /// don't get incorrectly released while they are still being used
pollCompletionSemaphore.wait() // Wait indefinitely for the timer semaphore syncQueue.sync { }
semaphoreResult = (iterations >= 50 ? .timedOut : .success)
} return logErrorIfNeeded(operationResult ?? .failure(StorageError.transactionDeadlockTimeout))
#else
semaphoreResult = semaphore?.wait(timeout: .now() + .seconds(Storage.transactionDeadlockTimeoutSeconds))
#endif
/// If the query timed out then we should interrupt the query (don't want the query thread to remain blocked when we've
/// already handled it as a failure) and need to call `completeOperation` as it wouldn't have been called within the
/// db transaction yet
if semaphoreResult == .timedOut {
syncQueue.sync { queryDb?.interrupt() }
completeOperation(with: .failure(StorageError.transactionDeadlockTimeout))
} }
/// Before returning we need to wait for any pending updates on `syncQueue` to be completed to ensure that objects /// For the `async` operation the returned value should be ignored so just return the `invalidQueryResult` error
/// don't get incorrectly released while they are still being used return .failure(StorageError.invalidQueryResult)
syncQueue.sync { }
return finalResult
} }
private func performPublisherOperation<T>( private func performPublisherOperation<T>(
@ -834,6 +812,42 @@ open class Storage {
} }
} }
private static func debugWait(semaphore: DispatchSemaphore, info: CallInfo) -> DispatchTimeoutResult {
let pollQueue: DispatchQueue = DispatchQueue(label: "com.session.debugWaitTimer.\(UUID().uuidString)")
let standardPollInterval: DispatchTimeInterval = .milliseconds(100)
var iterations: Int = 0
let maxIterations: Int = ((Storage.transactionDeadlockTimeoutSeconds * 1000) / standardPollInterval.milliseconds)
let pollCompletionSemaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
/// Stagger the size of the `pollIntervals` to avoid holding up the thread in case the query resolves very quickly (this
/// means the timeout will occur ~500ms early but helps prevent false main thread lag appearing when debugging that wouldn't
/// affect production)
let pollIntervals: [DispatchTimeInterval] = [
.milliseconds(5), .milliseconds(5), .milliseconds(10), .milliseconds(10), .milliseconds(10),
standardPollInterval
]
func pollSemaphore() {
iterations += 1
guard iterations < maxIterations && semaphore.wait(timeout: .now()) != .success else {
pollCompletionSemaphore.signal()
return
}
let nextInterval: DispatchTimeInterval = pollIntervals[min(iterations, pollIntervals.count - 1)]
pollQueue.asyncAfter(deadline: .now() + nextInterval) {
pollSemaphore()
}
}
/// Poll the semaphore in a background queue
pollQueue.asyncAfter(deadline: .now() + pollIntervals[0]) { pollSemaphore() }
pollCompletionSemaphore.wait() // Wait indefinitely for the timer semaphore
return (iterations >= 50 ? .timedOut : .success)
}
// MARK: - Functions // MARK: - Functions
@discardableResult public func write<T>( @discardableResult public func write<T>(

Loading…
Cancel
Save