Skip to content

Commit ae65b08

Browse files
committed
add support for duration additional type
1 parent f937efd commit ae65b08

File tree

6 files changed

+348
-11
lines changed

6 files changed

+348
-11
lines changed

Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension ProgressManager {
3333
let dirtyPropertiesString: [MetatypeWrapper<String?, [String?]>]
3434
let dirtyPropertiesURL: [MetatypeWrapper<URL?, [URL?]>]
3535
let dirtyPropertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>]
36+
let dirtyPropertiesDuration: [MetatypeWrapper<Duration, Duration>]
3637
#if FOUNDATION_FRAMEWORK
3738
let observerState: ObserverState?
3839
let interopType: InteropType?
@@ -67,6 +68,7 @@ extension ProgressManager {
6768
propertiesString: [:],
6869
propertiesURL: [:],
6970
propertiesUInt64Array: [:],
71+
propertiesDuration: [:],
7072
observers: [],
7173
interopType: nil,
7274
)
@@ -86,7 +88,8 @@ extension ProgressManager {
8688
propertiesDouble: [:],
8789
propertiesString: [:],
8890
propertiesURL: [:],
89-
propertiesUInt64Array: [:]
91+
propertiesUInt64Array: [:],
92+
propertiesDuration: [:]
9093
)
9194
#endif
9295
let result = try closure(&values)
@@ -108,6 +111,7 @@ extension ProgressManager {
108111
dirtyPropertiesString: values.dirtyPropertiesString,
109112
dirtyPropertiesURL: values.dirtyPropertiesURL,
110113
dirtyPropertiesUInt64Array: values.dirtyPropertiesUInt64Array,
114+
dirtyPropertiesDuration: values.dirtyPropertiesDuration,
111115
observerState: values.observerState,
112116
interopType: state.interopType
113117
)
@@ -126,7 +130,8 @@ extension ProgressManager {
126130
dirtyPropertiesDouble: values.dirtyPropertiesDouble,
127131
dirtyPropertiesString: values.dirtyPropertiesString,
128132
dirtyPropertiesURL: values.dirtyPropertiesURL,
129-
dirtyPropertiesUInt64Array: values.dirtyPropertiesUInt64Array
133+
dirtyPropertiesUInt64Array: values.dirtyPropertiesUInt64Array,
134+
dirtyPropertiesDuration: values.dirtyPropertiesDuration
130135
)
131136
#endif
132137

@@ -214,6 +219,13 @@ extension ProgressManager {
214219
markSelfDirty(property: property, parents: dirtyInfo.parents)
215220
}
216221
}
222+
223+
if dirtyInfo.dirtyPropertiesDuration.count > 0 {
224+
for property in dirtyInfo.dirtyPropertiesDuration {
225+
markSelfDirty(property: property, parents: dirtyInfo.parents)
226+
}
227+
}
228+
217229
return result
218230
}
219231

@@ -235,6 +247,7 @@ extension ProgressManager {
235247
internal var dirtyPropertiesString: [MetatypeWrapper<String?, [String?]>] = []
236248
internal var dirtyPropertiesURL: [MetatypeWrapper<URL?, [URL?]>] = []
237249
internal var dirtyPropertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>] = []
250+
internal var dirtyPropertiesDuration: [MetatypeWrapper<Duration, Duration>] = []
238251
#if FOUNDATION_FRAMEWORK
239252
internal var observerState: ObserverState?
240253
#endif
@@ -528,6 +541,29 @@ extension ProgressManager {
528541
}
529542
}
530543

544+
/// Gets or sets custom Duration properties.
545+
///
546+
/// This subscript provides read-write access to custom progress properties where the value
547+
/// type is `Duration` and the summary type is `Duration`. If the property has not been set,
548+
/// the getter returns the property's default value.
549+
///
550+
/// - Parameter key: A key path to the custom Duration property type.
551+
public subscript<P: Property>(dynamicMember key: KeyPath<ProgressManager.Properties, P.Type>) -> Duration where P.Value == Duration, P.Summary == Duration {
552+
get {
553+
return state.propertiesDuration[MetatypeWrapper(P.self)] ?? P.self.defaultValue
554+
}
555+
556+
set {
557+
guard newValue != state.propertiesDuration[MetatypeWrapper(P.self)] else {
558+
return
559+
}
560+
561+
state.propertiesDuration[MetatypeWrapper(P.self)] = newValue
562+
563+
dirtyPropertiesDuration.append(MetatypeWrapper(P.self))
564+
}
565+
}
566+
531567
#if FOUNDATION_FRAMEWORK
532568
private mutating func interopNotifications() {
533569
switch state.interopType {
@@ -633,6 +669,19 @@ extension ProgressManager {
633669
return getUpdatedUInt64ArraySummary(property: MetatypeWrapper(property))
634670
}
635671

672+
/// Returns a summary for a custom Duration property across the progress subtree.
673+
///
674+
/// This method aggregates the values of a custom Duration property from this progress manager
675+
/// and all its children, returning a consolidated summary value.
676+
///
677+
/// - Parameter property: The type of the Duration property to summarize. Must be a property
678+
/// where the value type is `Duration` and the summary type is `Duration`.
679+
/// - Returns: A `Duration` summary value for the specified property.
680+
public func summary<P: Property>(of property: P.Type) -> P.Summary where P.Value == Duration, P.Summary == Duration {
681+
accessObservation(keyPath: ProgressManager.additionalPropertiesKeyPath.withLock { $0 })
682+
return getUpdatedDurationSummary(property: MetatypeWrapper(property))
683+
}
684+
636685
/// Returns the total file count across the progress subtree.
637686
///
638687
/// - Parameter property: The `TotalFileCount` property type.

Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,23 @@ extension ProgressManager {
122122
}
123123
}
124124

125+
internal func getUpdatedDurationSummary(property: MetatypeWrapper<Duration, Duration>) -> Duration {
126+
// Collect information from state
127+
let updateInfo = state.withLock { state in
128+
state.getDurationSummaryUpdateInfo(property: property)
129+
}
130+
131+
// Get updated summary for each dirty child
132+
let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in
133+
State.DurationSummaryUpdate(index: index, updatedSummary: child.getUpdatedDurationSummary(property: property))
134+
}
135+
136+
// Consolidate updated values
137+
return state.withLock { state in
138+
state.updateDurationSummary(updateInfo, updatedSummaries)
139+
}
140+
}
141+
125142
internal func getUpdatedFileCount(type: CountType) -> Int {
126143
// Collect information from state
127144
let updateInfo = state.withLock { state in
@@ -240,6 +257,14 @@ extension ProgressManager {
240257
}
241258
}
242259

260+
internal func markSelfDirty(property: MetatypeWrapper<Duration, Duration>, parents: [ParentState]) {
261+
mutateObservation(of: ProgressManager.additionalPropertiesKeyPath.withLock { $0 }) {
262+
for parentState in parents {
263+
parentState.parent.markChildDirty(property: property, at: parentState.positionInParent)
264+
}
265+
}
266+
}
267+
243268
internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [ParentState]) {
244269
mutateObservation(of: ProgressManager.additionalPropertiesKeyPath.withLock { $0 }) {
245270
for parentState in parents {
@@ -329,6 +354,13 @@ extension ProgressManager {
329354
}
330355
markSelfDirty(property: property, parents: parents)
331356
}
357+
358+
internal func markChildDirty(property: MetatypeWrapper<Duration, Duration>, at position: Int) {
359+
let parents = state.withLock { state in
360+
state.markChildDirty(property: property, at: position)
361+
}
362+
markSelfDirty(property: property, parents: parents)
363+
}
332364

333365
internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) {
334366
let parents = state.withLock { state in
@@ -373,7 +405,7 @@ extension ProgressManager {
373405
}
374406

375407
//MARK: Method to preserve values of properties upon deinit
376-
internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, propertiesInt: [MetatypeWrapper<Int, Int>: Int], propertiesUInt64: [MetatypeWrapper<UInt64, UInt64>: UInt64], propertiesDouble: [MetatypeWrapper<Double, Double>: Double], propertiesString: [MetatypeWrapper<String?, [String?]>: [String?]], propertiesURL: [MetatypeWrapper<URL?, [URL?]>: [URL?]], propertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>: [UInt64]]) {
408+
internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, propertiesInt: [MetatypeWrapper<Int, Int>: Int], propertiesUInt64: [MetatypeWrapper<UInt64, UInt64>: UInt64], propertiesDouble: [MetatypeWrapper<Double, Double>: Double], propertiesString: [MetatypeWrapper<String?, [String?]>: [String?]], propertiesURL: [MetatypeWrapper<URL?, [URL?]>: [URL?]], propertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>: [UInt64]], propertiesDuration: [MetatypeWrapper<Duration, Duration>: Duration]) {
377409
state.withLock { state in
378410
state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false)
379411
state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false)
@@ -405,6 +437,10 @@ extension ProgressManager {
405437
for (propertyKey, propertyValue) in propertiesUInt64Array {
406438
state.children[position].childPropertiesUInt64Array[propertyKey] = PropertyStateThroughput(value: propertyValue, isDirty: false)
407439
}
440+
441+
for (propertyKey, propertyValue) in propertiesDuration {
442+
state.children[position].childPropertiesDuration[propertyKey] = PropertyStateDuration(value: propertyValue, isDirty: false)
443+
}
408444
}
409445
}
410446
}

Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ extension ProgressManager {
9595
var childPropertiesString: [MetatypeWrapper<String?, [String?]>: PropertyStateString]
9696
var childPropertiesURL: [MetatypeWrapper<URL?, [URL?]>: PropertyStateURL]
9797
var childPropertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>: PropertyStateThroughput]
98+
var childPropertiesDuration: [MetatypeWrapper<Duration, Duration>: PropertyStateDuration]
9899
}
99100

100101
internal struct ParentState {
@@ -130,6 +131,7 @@ extension ProgressManager {
130131
var propertiesString: [MetatypeWrapper<String?, [String?]>: String?]
131132
var propertiesURL: [MetatypeWrapper<URL?, [URL?]>: URL?]
132133
var propertiesUInt64Array: [MetatypeWrapper<UInt64, [UInt64]>: UInt64]
134+
var propertiesDuration: [MetatypeWrapper<Duration, Duration>: Duration]
133135
#if FOUNDATION_FRAMEWORK
134136
var observers: [@Sendable (ObserverState) -> Void]
135137
var interopType: InteropType?
@@ -286,6 +288,11 @@ extension ProgressManager {
286288
return parents
287289
}
288290

291+
internal mutating func markChildDirty(property: MetatypeWrapper<Duration, Duration>, at position: Int) -> [ParentState] {
292+
children[position].childPropertiesDuration[property]?.isDirty = true
293+
return parents
294+
}
295+
289296
internal mutating func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) -> [ParentState] {
290297
children[position].totalFileCount.isDirty = true
291298
return parents
@@ -389,6 +396,18 @@ extension ProgressManager {
389396
let updatedSummary: [UInt64]
390397
}
391398

399+
internal struct DurationSummaryUpdateInfo {
400+
let currentSummary: Duration
401+
let dirtyChildren: [(index: Int, manager: ProgressManager)]
402+
let nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)]
403+
let property: MetatypeWrapper<Duration, Duration>
404+
}
405+
406+
internal struct DurationSummaryUpdate {
407+
let index: Int
408+
let updatedSummary: Duration
409+
}
410+
392411
internal struct FileCountUpdateInfo {
393412
let currentSummary: Int
394413
let dirtyChildren: [(index: Int, manager: ProgressManager)]
@@ -813,6 +832,69 @@ extension ProgressManager {
813832
return value
814833
}
815834

835+
internal mutating func getDurationSummaryUpdateInfo(property: MetatypeWrapper<Duration, Duration>) -> DurationSummaryUpdateInfo {
836+
var currentSummary: Duration = property.defaultSummary
837+
property.reduce(&currentSummary, propertiesDuration[property] ?? property.defaultValue)
838+
839+
guard !children.isEmpty else {
840+
return DurationSummaryUpdateInfo(
841+
currentSummary: currentSummary,
842+
dirtyChildren: [],
843+
nonDirtySummaries: [],
844+
property: property
845+
)
846+
}
847+
848+
var dirtyChildren: [(index: Int, manager: ProgressManager)] = []
849+
var nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] = []
850+
851+
for (idx, childState) in children.enumerated() {
852+
if let childPropertyState = childState.childPropertiesDuration[property] {
853+
if childPropertyState.isDirty {
854+
if let child = childState.child {
855+
dirtyChildren.append((idx, child))
856+
}
857+
} else {
858+
let isAlive = childState.child != nil
859+
nonDirtySummaries.append((idx, childPropertyState.value, isAlive))
860+
}
861+
} else {
862+
// Property doesn't exist yet in child - need to fetch it
863+
if let child = childState.child {
864+
dirtyChildren.append((idx, child))
865+
}
866+
}
867+
}
868+
869+
return DurationSummaryUpdateInfo(
870+
currentSummary: currentSummary,
871+
dirtyChildren: dirtyChildren,
872+
nonDirtySummaries: nonDirtySummaries,
873+
property: property
874+
)
875+
}
876+
877+
internal mutating func updateDurationSummary(_ updateInfo: DurationSummaryUpdateInfo, _ childUpdates: [DurationSummaryUpdate]) -> Duration {
878+
var value = updateInfo.currentSummary
879+
880+
// Apply updates from children that were dirty
881+
for update in childUpdates {
882+
children[update.index].childPropertiesDuration[updateInfo.property] = PropertyStateDuration(value: update.updatedSummary, isDirty: false)
883+
value = updateInfo.property.merge(value, update.updatedSummary)
884+
}
885+
886+
// Apply values from non-dirty children
887+
for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries {
888+
if isAlive {
889+
value = updateInfo.property.merge(value, childSummary)
890+
} else {
891+
value = updateInfo.property.finalSummary(value, childSummary)
892+
}
893+
}
894+
895+
return value
896+
}
897+
816898
internal mutating func getFileCountUpdateInfo(type: CountType) -> FileCountUpdateInfo {
817899
let currentSummary: Int
818900
var dirtyChildren: [(index: Int, manager: ProgressManager)] = []

Sources/FoundationEssentials/ProgressManager/ProgressManager.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ internal import _FoundationCollections
9797
propertiesString: [:],
9898
propertiesURL: [:],
9999
propertiesUInt64Array: [:],
100+
propertiesDuration: [:],
100101
observers: [],
101102
interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge))
102103
)
@@ -120,6 +121,7 @@ internal import _FoundationCollections
120121
propertiesString: [:],
121122
propertiesURL: [:],
122123
propertiesUInt64Array: [:],
124+
propertiesDuration: [:]
123125
)
124126
self.state = Mutex(state)
125127
}
@@ -261,7 +263,8 @@ internal import _FoundationCollections
261263
childPropertiesDouble: [:],
262264
childPropertiesString: [:],
263265
childPropertiesURL: [:],
264-
childPropertiesUInt64Array: [:])
266+
childPropertiesUInt64Array: [:],
267+
childPropertiesDuration: [:])
265268
state.children.append(childState)
266269
return (state.children.count - 1, state.parents)
267270
}
@@ -312,8 +315,8 @@ internal import _FoundationCollections
312315

313316
deinit {
314317

315-
let (propertiesInt, propertiesUInt64, propertiesDouble, propertiesString, propertiesURL, propertiesUInt64Array, parents) = state.withLock { state in
316-
return (state.propertiesInt, state.propertiesUInt64, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.propertiesUInt64Array, state.parents)
318+
let (propertiesInt, propertiesUInt64, propertiesDouble, propertiesString, propertiesURL, propertiesUInt64Array, propertiesDuration, parents) = state.withLock { state in
319+
return (state.propertiesInt, state.propertiesUInt64, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.propertiesUInt64Array, state.propertiesDuration, state.parents)
317320
}
318321

319322
var finalSummaryInt: [MetatypeWrapper<Int, Int>: Int] = [:]
@@ -352,6 +355,12 @@ internal import _FoundationCollections
352355
finalSummaryUInt64Array[property] = updatedSummary
353356
}
354357

358+
var finalSummaryDuration: [MetatypeWrapper<Duration, Duration>: Duration] = [:]
359+
for property in propertiesDuration.keys {
360+
let updatedSummary = self.getUpdatedDurationSummary(property: property)
361+
finalSummaryDuration[property] = updatedSummary
362+
}
363+
355364
let totalFileCount = self.getUpdatedFileCount(type: .total)
356365
let completedFileCount = self.getUpdatedFileCount(type: .completed)
357366
let totalByteCount = self.getUpdatedByteCount(type: .total)
@@ -377,7 +386,8 @@ internal import _FoundationCollections
377386
propertiesDouble: finalSummaryDouble,
378387
propertiesString: finalSummaryString,
379388
propertiesURL: finalSummaryURL,
380-
propertiesUInt64Array: finalSummaryUInt64Array
389+
propertiesUInt64Array: finalSummaryUInt64Array,
390+
propertiesDuration: finalSummaryDuration
381391
)
382392
}
383393
}

Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ import Observation
134134
return manager.summary(of: property)
135135
}
136136

137+
public func summary<P: ProgressManager.Property>(of property: P.Type) -> Duration where P.Value == Duration, P.Summary == Duration {
138+
return manager.summary(of: property)
139+
}
140+
137141
/// Returns the total file count across the progress subtree.
138142
///
139143
/// - Parameter property: The `TotalFileCount` property type.

0 commit comments

Comments
 (0)