fix(ios,android): implement rolling window counting, TTL validation, and DB persistence

- iOS: Implement rolling window counting using UNUserNotificationCenter
- iOS: Enable TTL validation in scheduler before arming notifications
- iOS: Implement SQLite persistence for save/delete/clear operations
- Android: Implement rolling window counting using storage as source of truth
- Android: Add dateBoundsMillis helper for date range calculations

Removes all TODO stubs affecting capacity/rate-limiting correctness.
Fixes runtime behavior to match test expectations and optimizer logic.

Refs: Deep fixes directive for bottom-of-tree gaps
This commit is contained in:
Matthew Raymer
2025-12-24 04:11:41 +00:00
parent 9b73e873d9
commit d8b29954a2
4 changed files with 233 additions and 33 deletions

View File

@@ -287,6 +287,25 @@ class DailyNotificationRollingWindow {
// MARK: - Data Access
/**
* Fetch pending notification requests synchronously
*
* @param timeoutSeconds Timeout in seconds
* @return Array of pending notification requests
*/
private func fetchPendingRequestsSync(timeoutSeconds: TimeInterval) -> [UNNotificationRequest] {
let sem = DispatchSemaphore(value: 0)
var result: [UNNotificationRequest] = []
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
result = requests
sem.signal()
}
_ = sem.wait(timeout: .now() + timeoutSeconds)
return result
}
/**
* Count pending notifications
*
@@ -294,10 +313,8 @@ class DailyNotificationRollingWindow {
*/
private func countPendingNotifications() -> Int {
do {
// This would typically query the storage for pending notifications
// For now, we'll use a placeholder implementation
return 0 // TODO: Implement actual counting logic
let requests = fetchPendingRequestsSync(timeoutSeconds: 2.0)
return requests.count
} catch {
print("\(Self.TAG): Error counting pending notifications: \(error)")
return 0
@@ -312,10 +329,18 @@ class DailyNotificationRollingWindow {
*/
private func countNotificationsForDate(_ date: String) -> Int {
do {
// This would typically query the storage for notifications on a specific date
// For now, we'll use a placeholder implementation
return 0 // TODO: Implement actual counting logic
let requests = fetchPendingRequestsSync(timeoutSeconds: 2.0)
var count = 0
for req in requests {
guard let trigger = req.trigger as? UNCalendarNotificationTrigger else { continue }
guard let nextDate = trigger.nextTriggerDate() else { continue }
if formatDate(nextDate) == date {
count += 1
}
}
return count
} catch {
print("\(Self.TAG): Error counting notifications for date: \(date), error: \(error)")
return 0
@@ -330,10 +355,42 @@ class DailyNotificationRollingWindow {
*/
private func getNotificationsForDate(_ date: String) -> [NotificationContent] {
do {
// This would typically query the storage for notifications on a specific date
// For now, we'll return an empty array
return [] // TODO: Implement actual retrieval logic
let requests = fetchPendingRequestsSync(timeoutSeconds: 2.0)
var results: [NotificationContent] = []
for req in requests {
guard let trigger = req.trigger as? UNCalendarNotificationTrigger else { continue }
guard let nextDate = trigger.nextTriggerDate() else { continue }
if formatDate(nextDate) != date { continue }
// We cannot reconstruct full NotificationContent from UNNotificationRequest reliably,
// so this returns minimal stubs primarily for internal rolling-window inspection.
let id = req.identifier
let scheduledMs = Int64(nextDate.timeIntervalSince1970 * 1000.0)
let fetchedMs: Int64
if let fetchedAt = req.content.userInfo["fetched_at"] as? Int64 {
fetchedMs = fetchedAt
} else if let fetchedAt = req.content.userInfo["fetched_at"] as? Int {
fetchedMs = Int64(fetchedAt)
} else {
fetchedMs = scheduledMs
}
let stub = NotificationContent(
id: id,
title: req.content.title,
body: req.content.body,
scheduledTime: scheduledMs,
fetchedAt: fetchedMs,
url: nil,
payload: nil,
etag: nil
)
results.append(stub)
}
return results
} catch {
print("\(Self.TAG): Error getting notifications for date: \(date), error: \(error)")
return []