問題
`MetronomeViewModel.scheduleFollowerTimeout` が `Task.sleep(nanoseconds:)` でタイムアウトを実装しているが、wake 精度が scheduler 依存で数十 ms の overshoot がありうる。
現状
```swift
followerTimeoutTask = Task { [weak self] in
let timeoutNs = max(intervalNs * 3 / 2, 400_000_000)
try? await Task.sleep(nanoseconds: timeoutNs)
...
await self?.followerTimedOut()
}
```
改善方針
Task.sleep は「状態遷移のきっかけ」にとどめる。実際の次クリックは wake 時点の現在時刻を基準に未来ビートを再計算して schedule する:
```swift
private func followerTimedOut() async {
...
// Don't rely on "lastBeatHostTime + 1 beat" — may be past
let now = mach_absolute_time()
let beatInterval = subIntervalMach * UInt64(subsPerBeat)
var nextBeat = lastExternalHostTime + beatInterval
while nextBeat < now { nextBeat += beatInterval; beatIndex += 1 }
...
}
```
重要度
Medium(#26 と併せて対応)
関連 issue
#26
問題
`MetronomeViewModel.scheduleFollowerTimeout` が `Task.sleep(nanoseconds:)` でタイムアウトを実装しているが、wake 精度が scheduler 依存で数十 ms の overshoot がありうる。
現状
```swift
followerTimeoutTask = Task { [weak self] in
let timeoutNs = max(intervalNs * 3 / 2, 400_000_000)
try? await Task.sleep(nanoseconds: timeoutNs)
...
await self?.followerTimedOut()
}
```
改善方針
Task.sleep は「状態遷移のきっかけ」にとどめる。実際の次クリックは wake 時点の現在時刻を基準に未来ビートを再計算して schedule する:
```swift
private func followerTimedOut() async {
...
// Don't rely on "lastBeatHostTime + 1 beat" — may be past
let now = mach_absolute_time()
let beatInterval = subIntervalMach * UInt64(subsPerBeat)
var nextBeat = lastExternalHostTime + beatInterval
while nextBeat < now { nextBeat += beatInterval; beatIndex += 1 }
...
}
```
重要度
Medium(#26 と併せて対応)
関連 issue
#26