Skip to content

Commit e3f7c7c

Browse files
authored
Merge branch 'master' into feature/209-due-by-table-deltas-on-gallery
2 parents dc1ef87 + 875229a commit e3f7c7c

25 files changed

+784
-264
lines changed

.github/workflows/fastlane-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: maxim-lobanov/setup-xcode@v1
1616
with:
1717
xcode-version: latest-stable
18-
- uses: actions/checkout@v4
18+
- uses: actions/checkout@v5
1919
- name: Use sample configuration
2020
run: cp BeeKit/Config.swift.sample BeeKit/Config.swift
2121
- name: Setup ruby and install gems

BeeKit/Managers/DataPointManager.swift

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public actor DataPointManager {
3030
}
3131
}
3232

33-
private func updateDatapoint(goal : Goal, datapoint : DataPoint, datapointValue : NSNumber) async throws {
33+
private func updateDatapoint(goal : Goal, datapoint : DataPoint, datapointValue : NSNumber, comment: String) async throws {
3434
let val = datapoint.value
35-
if datapointValue == val {
35+
if datapointValue == val && comment == datapoint.comment {
3636
return
3737
}
3838
let params = [
3939
"value": "\(datapointValue)",
40-
"comment": "Auto-updated via Apple Health",
40+
"comment": comment,
4141
]
4242
let _ = try await requestManager.put(url: "api/v1/users/{username}/goals/\(goal.slug)/datapoints/\(datapoint.id).json", parameters: params)
4343
}
@@ -103,37 +103,52 @@ public actor DataPointManager {
103103
let datapoints = try await datapointsSince(goal: goal, daystamp: try! Daystamp(fromString: firstDaystamp.description))
104104
let realDatapoints = datapoints.filter{ !$0.isDummy && !$0.isInitial }
105105

106-
for newDataPoint in healthKitDataPoints {
107-
try await self.updateToMatchDataPoint(goal: goal, newDataPoint: newDataPoint, recentDatapoints: realDatapoints)
106+
let healthKitDataPointsByDay = Dictionary(grouping: healthKitDataPoints) { $0.daystamp }
107+
108+
try await withThrowingTaskGroup(of: Void.self) { group in
109+
for (daystamp, dayDataPoints) in healthKitDataPointsByDay {
110+
group.addTask {
111+
let existingDatapointsForDay = await self.datapointsMatchingDaystamp(datapoints: realDatapoints, daystamp: daystamp)
112+
try await self.updateToMatchDataPointsForDay(goal: goal, newDataPoints: dayDataPoints, existingDatapoints: existingDatapointsForDay)
113+
}
114+
}
108115
}
109116
}
110117

111-
private func updateToMatchDataPoint(goal: Goal, newDataPoint : BeeDataPoint, recentDatapoints: [DataPoint]) async throws {
112-
var matchingDatapoints = datapointsMatchingDaystamp(datapoints: recentDatapoints, daystamp: newDataPoint.daystamp)
113-
if matchingDatapoints.count == 0 {
114-
// If there are not already data points for this day, do not add points
115-
// from before the creation of the goal. This avoids immediate derailment
116-
//on do less goals, and excessive safety buffer on do-more goals.
117-
if newDataPoint.daystamp < goal.initDaystamp {
118-
return
118+
private func updateToMatchDataPointsForDay(goal: Goal, newDataPoints: [BeeDataPoint], existingDatapoints: [DataPoint]) async throws {
119+
try await withThrowingTaskGroup(of: Void.self) { group in
120+
var processedDatapoints: Set<String> = []
121+
122+
for newDataPoint in newDataPoints {
123+
let matchingDatapoint = existingDatapoints.first { $0.requestid == newDataPoint.requestid }
124+
125+
if let existingDatapoint = matchingDatapoint {
126+
if !isApproximatelyEqual(existingDatapoint.value.doubleValue, newDataPoint.value.doubleValue) || existingDatapoint.comment != newDataPoint.comment {
127+
group.addTask {
128+
self.logger.notice("Updating datapoint for \(goal.id) with requestId \(newDataPoint.requestid, privacy: .public) from \(existingDatapoint.value) to \(newDataPoint.value)")
129+
try await self.updateDatapoint(goal: goal, datapoint: existingDatapoint, datapointValue: newDataPoint.value, comment: newDataPoint.comment)
130+
}
131+
}
132+
processedDatapoints.insert(existingDatapoint.requestid)
133+
} else if newDataPoint.daystamp >= goal.initDaystamp {
134+
// If there are not already data points for this requestId, do not add points
135+
// from before the creation of the goal. This avoids immediate derailment
136+
// on do less goals, and excessive safety buffer on do-more goals.
137+
group.addTask {
138+
let urText = "\(newDataPoint.daystamp.day) \(newDataPoint.value) \"\(newDataPoint.comment)\""
139+
self.logger.notice("Creating new datapoint for \(goal.id, privacy: .public) with requestId \(newDataPoint.requestid, privacy: .public): \(newDataPoint.value, privacy: .private)")
140+
try await self.postDatapoint(goal: goal, urText: urText, requestId: newDataPoint.requestid)
141+
}
142+
}
119143
}
120-
121-
let urText = "\(newDataPoint.daystamp.day) \(newDataPoint.value) \"\(newDataPoint.comment)\""
122-
let requestId = newDataPoint.requestid
123-
124-
logger.notice("Creating new datapoint for \(goal.id, privacy: .public) on \(newDataPoint.daystamp, privacy: .public): \(newDataPoint.value, privacy: .private)")
125-
126-
try await postDatapoint(goal: goal, urText: urText, requestId: requestId)
127-
} else if matchingDatapoints.count >= 1 {
128-
let firstDatapoint = matchingDatapoints.remove(at: 0)
129-
for datapoint in matchingDatapoints {
130-
try await deleteDatapoint(goal: goal, datapoint: datapoint)
131-
}
132-
133-
if !isApproximatelyEqual(firstDatapoint.value.doubleValue, newDataPoint.value.doubleValue) {
134-
logger.notice("Updating datapoint for \(goal.id) on \(firstDatapoint.daystamp, privacy: .public) from \(firstDatapoint.value) to \(newDataPoint.value)")
135-
136-
try await updateDatapoint(goal: goal, datapoint: firstDatapoint, datapointValue: newDataPoint.value)
144+
145+
for existingDatapoint in existingDatapoints {
146+
if !processedDatapoints.contains(existingDatapoint.requestid) {
147+
group.addTask {
148+
self.logger.notice("Deleting obsolete datapoint for \(goal.id) with requestId \(existingDatapoint.requestid, privacy: .public)")
149+
try await self.deleteDatapoint(goal: goal, datapoint: existingDatapoint)
150+
}
151+
}
137152
}
138153
}
139154
}

BeeKit/Managers/GoalManager.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ public actor GoalManager {
7474
// We must fetch the user object first, and then fetch goals afterwards, to guarantee User.updated_at is
7575
// a safe timestamp for future fetches without losing data
7676
let userResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}.json")!)
77-
let goalResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}/goals.json")!)
78-
77+
let goalResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}/goals.json", parameters: ["emaciated": "true"])!)
78+
7979
// The user may have logged out during the network operation. If so we have nothing to do
8080
modelContext.refreshAllObjects()
8181
guard let user = modelContext.object(with: user.objectID) as? User else { return }
@@ -97,7 +97,7 @@ public actor GoalManager {
9797
/// Perform an incremental refresh of goals for regular updates
9898
private func refreshGoalsIncremental(user: User) async throws {
9999
logger.notice("Doing incremental update since \(user.updatedAt, privacy: .public)")
100-
let userResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}.json", parameters: ["diff_since": user.updatedAt.timeIntervalSince1970 + 1])!)
100+
let userResponse = JSON(try await requestManager.get(url: "api/v1/users/{username}.json", parameters: ["diff_since": user.updatedAt.timeIntervalSince1970 + 1, "emaciated": "true"])!)
101101
let goalResponse = userResponse["goals"]
102102
let deletedGoals = userResponse["deleted_goals"]
103103

@@ -126,7 +126,8 @@ public actor GoalManager {
126126
public func refreshGoal(_ goalID: NSManagedObjectID) async throws {
127127
let goal = try modelContext.existingObject(with: goalID) as! Goal
128128

129-
let responseObject = try await requestManager.get(url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)?datapoints_count=5")
129+
let responseObject = try await requestManager.get(url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)",
130+
parameters: ["datapoints_count": "5", "emaciated": "true"])
130131
let goalJSON = JSON(responseObject!)
131132

132133
// The goal may have changed during the network operation, reload latest version
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Part of BeeSwift. Copyright Beeminder
2+
3+
import Foundation
4+
import OSLog
5+
6+
public class RefreshManager {
7+
private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "RefreshManager")
8+
private let healthStoreManager: HealthStoreManager
9+
private let goalManager: GoalManager
10+
11+
public init(healthStoreManager: HealthStoreManager, goalManager: GoalManager) {
12+
self.healthStoreManager = healthStoreManager
13+
self.goalManager = goalManager
14+
}
15+
16+
@MainActor
17+
public func refreshGoalsAndHealthKitData() async {
18+
await withTaskGroup(of: Void.self) { group in
19+
group.addTask {
20+
do {
21+
let _ = try await self.healthStoreManager.updateAllGoalsWithRecentData(days: 7)
22+
} catch {
23+
self.logger.error("Error updating from healthkit: \(error)")
24+
}
25+
}
26+
27+
group.addTask {
28+
do {
29+
try await self.goalManager.refreshGoals()
30+
} catch {
31+
self.logger.error("Error refreshing goals: \(error)")
32+
}
33+
}
34+
}
35+
}
36+
}

BeeKit/ServiceLocator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ public class ServiceLocator {
2121
public static let dataPointManager = DataPointManager(requestManager: requestManager, container: persistentContainer)
2222
public static let healthStoreManager = HealthStoreManager(goalManager: goalManager, container: persistentContainer)
2323
public static let versionManager = VersionManager(requestManager: requestManager)
24+
public static let refreshManager = RefreshManager(healthStoreManager: healthStoreManager, goalManager: goalManager)
2425
}

0 commit comments

Comments
 (0)