You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm writing a wrapper for @Shared to replace an old singleton as well as a newer swift-dependencies-style wrapper. Currently they each do their own caching and operate in separate parts of the app, so unifying them will help keep things in sync.
However, while I've gotten the property wrapper to work, I'm having trouble optimizing it. Namely, I track the Tasks which encapsulate the network calls used to update the underlying state. However, this means that the shared state is updated, which triggers the observation and publisher machinery. I'd like to avoid that but there doesn't seem to be any way for @Shared to ignore updates to particular properties, nor does @PerceptionIgnored work.
Alternatively I could break those Tasks out into their own @Shared value, but then I run into issues properly keeping updates atomic. Since every instance of the wrapper is separate, internal locks won't help. Moving everything to use the async withLock (which seems misnamed since it doesn't lock and has to be used async) causes issues, as it means I can no longer atomically update the state in a way that prevents duplicate work, as the suspension provides an opportunity for other instances to start their own version of the work.
Is there any general guidance here, for either of these problems?
Current wrapper code
@propertyWrapperstructFeatures:Equatable{structGroupKey:Hashable{letgroup:FeatureResponse.GroupletuserID:Stringinit?(group:FeatureResponse.Group){@Dependency(\.user)varuserguardlet userID =user()?.userId else{returnnil}self.userID = userID
self.group = group
}}@Shared(.inMemory("SharedFeatureAssessments"))privatevarfeatures=SharedFeatures()varprojectedValue:Self{self}varwrappedValue:SharedFeatures{get{self.features }nonmutating set{ features = newValue }}
/// Underlying shared publisher.
///
/// - Note: This publisher emits when any of the `isLoading`, `response`, or `task` values are updated, despite the
/// task values not being visible externally.
///
varpublisher:AnyPublisher<SharedFeatureAssessments,Never>{self.$assessments.publisher
}func refresh(group:FeatureResponse.Group){Task{awaitself.refresh(group: group)}}func refresh(group:FeatureResponse.Group)async{guardlet key =Features.GroupKey(group: group)else{return}@Dependency(\.featuresClient)varfeaturesClientself.features.isLoading[key]=truevarisNewTask=falselettask:Task<FeatureResponse?,Never>iflet existingTask = features.refreshTasks[key]{
task = existingTask
}else{letnewTask=Task{await features.fetchAvailability(group)}self.features.refreshTasks[key]= newTask
isNewTask =true
task = newTask
}letresponse=await task.value
if isNewTask {
// Mutate atomically.
self.mutate{
$0.refreshTasks[key]=nil
$0.responses[key]= response
$0.isLoading[key]=false}}}func refreshAll(){self.makeRefreshTasks()}func refreshAll()async{fortaskinself.makeRefreshTasks(){await task.value
}}privatefunc mutate(_ mutation:(inoutSharedFeatures)->Void){mutation(&self.features)}@discardableResultprivatefunc makeRefreshTasks()->[Task<Void,Never>]{self.features.responses.keys.map(\.group).uniqued().map{ group in
// Start refreshes in parallel.
Task{awaitself.refresh(group: group)}}}}structSharedFeatures:Equatable{
subscript(response group:FeatureResponse.FeatureGroup)->FeatureResponse?{get{Features.GroupKey(group: group).flatMap{self.responses[$0]}}set{guardlet key =Features.GroupKey(group: group)else{return}self.responses[key]= newValue
}}
subscript(isLoading group:FeatureResponse.Group)->Bool{get{Features.GroupKey(group: group).flatMap{self.isLoading[$0]}??false}set{guardlet key =Features.GroupKey(group: group)else{return}self.isLoading[key]= newValue
}}fileprivatevarresponses:[Features.GroupKey:FeatureResponse]=[:]fileprivatevarisLoading:[Features.GroupKey:Bool]=[:]fileprivatevarrefreshTasks:[Features.GroupKey:Task<FeatureResponse?,Never>]=[:]}
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm writing a wrapper for
@Shared
to replace an old singleton as well as a newerswift-dependencies
-style wrapper. Currently they each do their own caching and operate in separate parts of the app, so unifying them will help keep things in sync.However, while I've gotten the property wrapper to work, I'm having trouble optimizing it. Namely, I track the
Task
s which encapsulate the network calls used to update the underlying state. However, this means that the shared state is updated, which triggers the observation and publisher machinery. I'd like to avoid that but there doesn't seem to be any way for@Shared
to ignore updates to particular properties, nor does@PerceptionIgnored
work.Alternatively I could break those
Task
s out into their own@Shared
value, but then I run into issues properly keeping updates atomic. Since every instance of the wrapper is separate, internal locks won't help. Moving everything to use the asyncwithLock
(which seems misnamed since it doesn't lock and has to be used async) causes issues, as it means I can no longer atomically update the state in a way that prevents duplicate work, as the suspension provides an opportunity for other instances to start their own version of the work.Is there any general guidance here, for either of these problems?
Current wrapper code
Beta Was this translation helpful? Give feedback.
All reactions