Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Trio.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@
AABB00022C54389F00211FAC /* TreatmentsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABB00012C54389F00211FAC /* TreatmentsSettingsView.swift */; };
AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */; };
AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
B015AFF32E600000000D7351 /* AdjustmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFF12E600000000D7351 /* AdjustmentManager.swift */; };
B015AFF52E600000000D7351 /* AdjustmentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFF42E600000000D7351 /* AdjustmentManagerTests.swift */; };
B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */; };
B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */; };
B63C9D934FC54853BC4EF29A /* NightscoutUploadSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E61A0885164081B57C357C /* NightscoutUploadSerializer.swift */; };
Expand Down Expand Up @@ -1483,6 +1485,8 @@
AABB00012C54389F00211FAC /* TreatmentsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentsSettingsView.swift; sourceTree = "<group>"; };
AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
B015AFF12E600000000D7351 /* AdjustmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentManager.swift; sourceTree = "<group>"; };
B015AFF42E600000000D7351 /* AdjustmentManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentManagerTests.swift; sourceTree = "<group>"; };
B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidator.swift; sourceTree = "<group>"; };
B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidatorTests.swift; sourceTree = "<group>"; };
B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2512,6 +2516,7 @@
3811DE9125C9D88200A708ED /* Services */ = {
isa = PBXGroup;
children = (
B015AFF22E600000000D7351 /* Adjustments */,
BD1179262F4E22C100F90001 /* Alerts */,
3811DE9225C9D88200A708ED /* Appearance */,
DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
Expand Down Expand Up @@ -3111,6 +3116,7 @@
DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
B015AFF62E600000000D7351 /* AdjustmentsTests */,
B015AFE62E500000000D7351 /* BolusSafetyTests */,
BD8FC0552D66187700B95AED /* CoreDataTests */,
38FCF3F125E9028E0078B0D1 /* Info.plist */,
Expand Down Expand Up @@ -3518,6 +3524,22 @@
path = BasalProfileEditor;
sourceTree = "<group>";
};
B015AFF22E600000000D7351 /* Adjustments */ = {
isa = PBXGroup;
children = (
B015AFF12E600000000D7351 /* AdjustmentManager.swift */,
);
path = Adjustments;
sourceTree = "<group>";
};
B015AFF62E600000000D7351 /* AdjustmentsTests */ = {
isa = PBXGroup;
children = (
B015AFF42E600000000D7351 /* AdjustmentManagerTests.swift */,
);
path = AdjustmentsTests;
sourceTree = "<group>";
};
B015AFE22E500000000D7351 /* BolusSafety */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5448,6 +5470,7 @@
38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
B015AFF32E600000000D7351 /* AdjustmentManager.swift in Sources */,
B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */,
DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */,
38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
Expand Down Expand Up @@ -5920,6 +5943,7 @@
DD30BA002E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift in Sources */,
BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,
41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */,
B015AFF52E600000000D7351 /* AdjustmentManagerTests.swift in Sources */,
B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */,
BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */,
CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions Trio/Sources/APS/APSManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ protocol APSManager {
func enactTempBasal(rate: Double, duration: TimeInterval) async
func determineBasal() async throws
func determineBasalSync() async throws
/// Recomputes the determination after something the algorithm reads has changed — an override,
/// a temp target. Skips itself while a loop is running, because that loop determines from the
/// same state, and never delays a loop start.
func recomputeDetermination() async
func simulateDetermineBasal(
simulatedCarbsAmount: Decimal,
simulatedBolusAmount: Decimal,
Expand Down Expand Up @@ -76,6 +80,7 @@ enum APSError: LocalizedError {
/// Ensures only one loop runs at a time via actor isolation
private actor LoopGuard {
private var isRunning = false
private var isRecomputing = false

/// Atomically checks whether a new loop can start and marks it as running if so.
func tryStart(minInterval: TimeInterval, lastLoopDate: Date, lastLoopStartDate: Date) -> Bool {
Expand All @@ -91,6 +96,19 @@ private actor LoopGuard {
func finish() {
isRunning = false
}

/// Claims the guard for a determination-only recompute. Refuses while a loop runs, and while
/// another recompute runs — a second one would compute the same state. `tryStart` ignores this
/// claim: a skipped loop has a therapy cost, an overlapping recompute only wastes work.
func tryStartRecompute() -> Bool {
guard !isRunning, !isRecomputing else { return false }
isRecomputing = true
return true
}

func finishRecompute() {
isRecomputing = false
}
}

final class BaseAPSManager: APSManager, Injectable {
Expand Down Expand Up @@ -558,6 +576,19 @@ final class BaseAPSManager: APSManager, Injectable {
_ = try await determineBasal()
}

func recomputeDetermination() async {
guard await loopGuard.tryStartRecompute() else {
debug(.apsManager, "Determination recompute skipped: loop or recompute in flight")
return
}
do {
try await determineBasal()
} catch {
debug(.apsManager, "Determination recompute failed: \(error)")
}
await loopGuard.finishRecompute()
}

func simulateDetermineBasal(
simulatedCarbsAmount: Decimal,
simulatedBolusAmount: Decimal,
Expand Down
2 changes: 1 addition & 1 deletion Trio/Sources/APS/Storage/OverrideStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
ofType: OverrideRunStored.self,
onContext: context,
predicate: NSPredicate(
format: "startDate >= %@ AND isUploadedToNS == %@",
format: "endDate >= %@ AND isUploadedToNS == %@",
Date.oneDayAgo as NSDate,
false as NSNumber
),
Expand Down
15 changes: 9 additions & 6 deletions Trio/Sources/APS/Storage/TempTargetsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ protocol TempTargetsObserver {
}

protocol TempTargetsStorage {
func storeTempTarget(tempTarget: TempTarget) async throws
@discardableResult func storeTempTarget(tempTarget: TempTarget) async throws -> NSManagedObjectID
func saveTempTargetsToStorage(_ targets: [TempTarget])
func fetchForTempTargetPresets() async throws -> [NSManagedObjectID]
func fetchScheduledTempTargets() async throws -> [NSManagedObjectID]
Expand Down Expand Up @@ -131,7 +131,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
}
}

func storeTempTarget(tempTarget: TempTarget) async throws {
@discardableResult func storeTempTarget(tempTarget: TempTarget) async throws -> NSManagedObjectID {
let context = makeContext()
context.name = "storeTempTarget"

Expand All @@ -141,7 +141,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
presetCount = presets.count
}

try await context.perform {
return try await context.perform {
let newTempTarget = TempTargetStored(context: context)
newTempTarget.date = tempTarget.createdAt
newTempTarget.id = UUID()
Expand All @@ -167,12 +167,15 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
}

do {
guard context.hasChanges else { return }
try context.save()
if context.hasChanges {
try context.save()
}
} catch let error as NSError {
debug(.default, "\(DebuggingIdentifiers.failed) Failed to save new temp target with error: \(error.userInfo)")
throw error
}

return newTempTarget.objectID
}
}

Expand Down Expand Up @@ -336,7 +339,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
ofType: TempTargetRunStored.self,
onContext: context,
predicate: NSPredicate(
format: "startDate >= %@ AND isUploadedToNS == %@",
format: "endDate >= %@ AND isUploadedToNS == %@",
Date.oneDayAgo as NSDate,
false as NSNumber
),
Expand Down
4 changes: 4 additions & 0 deletions Trio/Sources/Assemblies/ServiceAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ final class ServiceAssembly: Assembly {
}
container.register(IOBService.self) { r in BaseIOBService(resolver: r) }
container.register(BolusSafetyValidator.self) { r in BaseBolusSafetyValidator(resolver: r) }
// Container scope: the manager serializes adjustment mutations across every entry point,
// so all callers share one instance.
container.register(AdjustmentManager.self) { r in BaseAdjustmentManager(resolver: r) }
.inObjectScope(.container)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,7 @@ extension Adjustments.StateModel {
await self.updateLatestOverrideConfigurationOfState(from: id)
await self.setCurrentOverride(from: id)

// perform determine basal sync to immediately apply override changes
try await apsManager.determineBasalSync()
await apsManager.recomputeDetermination()
} catch {
debug(
.default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ extension Adjustments.StateModel {
async let setTempTarget: () = setCurrentTempTarget(from: id)
_ = await (updateState, setTempTarget)

// perform determine basal sync to immediately apply temp target changes
try await apsManager.determineBasalSync()
await apsManager.recomputeDetermination()
} catch {
debug(
.default,
Expand Down
Loading
Loading