Make the live-run mirror survive lost phone-to-watch frames
Live frames ride sendMessage, which is reachable-only — and phone→watch reachability drops exactly when the user is swiping on the phone (wrist down). A frame that failed to send was staged but never retried until a reachability edge, and a frame lost outright desynced the run until the next human transition — sometimes forever, since a reconnect could even re-send the stale staged frame and yank the peer backwards. Four fixes, symmetric on both bridges and both run screens: - A send that fails while nominally reachable now retries with a short backoff (a few times per staged message) instead of being swallowed. - Receiving a frame that outranks the staged outbound one drops the staged frame, so a reconnect re-send can't move the run backwards; a delivery the staged frame outranks is ignored as stale. - Every staleness comparison now tie-breaks the shared version sequence on the frame's wall-clock anchor (LiveProgress.isNewer) — after a lost frame both devices can mint the same version, and the later human action must win. - Durable repair: when the absorbed workout doc shows sets completed beyond anything the open run screen recorded or followed, it jumps forward to the first unfinished set's work page — a lost frame now degrades to a briefly-stale page instead of a stuck one.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Workouts
|
||||
|
||||
/// Pins the live-mirror staleness ordering (`LiveProgress.isNewer`): the shared per-run
|
||||
/// version sequence decides, and the phase anchor breaks ties. The tie-break exists
|
||||
/// because the sequence isn't collision-free — each side catches its counter up only from
|
||||
/// frames it *receives*, so after a lost frame both devices can mint the same version
|
||||
/// independently, and the later wall-clock action must win. Equal version *and* equal
|
||||
/// anchor is the same frame redelivered, which must NOT rank as newer (a redelivery would
|
||||
/// otherwise re-jump the follower's page).
|
||||
struct LiveProgressOrderingTests {
|
||||
private static let t0 = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
|
||||
private func frame(version: Int, start: Date) -> LiveProgress {
|
||||
LiveProgress(workoutID: "W", logID: "L", exerciseName: "Bench", phase: .work,
|
||||
setIndex: 0, setCount: 3, detail: "8 reps",
|
||||
phaseStart: start, phaseEnd: nil, version: version)
|
||||
}
|
||||
|
||||
@Test func higherVersionWinsRegardlessOfAnchor() {
|
||||
let older = frame(version: 3, start: Self.t0.addingTimeInterval(10))
|
||||
let newer = frame(version: 4, start: Self.t0) // earlier anchor, higher version
|
||||
#expect(newer.isNewer(than: older))
|
||||
#expect(!older.isNewer(than: newer))
|
||||
}
|
||||
|
||||
@Test func collidedVersionsTieBreakOnAnchor() {
|
||||
let phoneFrame = frame(version: 4, start: Self.t0)
|
||||
let watchFrame = frame(version: 4, start: Self.t0.addingTimeInterval(2))
|
||||
#expect(watchFrame.isNewer(than: phoneFrame))
|
||||
#expect(!phoneFrame.isNewer(than: watchFrame))
|
||||
}
|
||||
|
||||
@Test func redeliveryIsNotNewer() {
|
||||
let sent = frame(version: 4, start: Self.t0)
|
||||
#expect(!sent.isNewer(than: sent))
|
||||
}
|
||||
|
||||
@Test func versionAndStartOverloadMatchesFrameOverload() {
|
||||
let f = frame(version: 4, start: Self.t0)
|
||||
#expect(f.isNewer(thanVersion: 3, start: Self.t0.addingTimeInterval(99)))
|
||||
#expect(f.isNewer(thanVersion: 4, start: Self.t0.addingTimeInterval(-1)))
|
||||
#expect(!f.isNewer(thanVersion: 4, start: Self.t0))
|
||||
#expect(!f.isNewer(thanVersion: 5, start: .distantPast))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user