You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

393 lines
14 KiB

/**
* DailyNotificationTTLEnforcer.swift
*
* iOS TTL-at-fire enforcement for notification freshness
* Implements the skip rule: if (T - fetchedAt) > ttlSeconds skip arming
*
* @author Matthew Raymer
* @version 1.0.0
*/
import Foundation
/**
* Enforces TTL-at-fire rules for notification freshness on iOS
*
* This class implements the critical freshness enforcement:
* - Before arming for T, if (T fetchedAt) > ttlSeconds skip
* - Logs TTL violations for debugging
* - Supports both SQLite and UserDefaults storage
* - Provides freshness validation before scheduling
*/
class DailyNotificationTTLEnforcer {
// MARK: - Constants
private static let TAG = "DailyNotificationTTLEnforcer"
private static let LOG_CODE_TTL_VIOLATION = "TTL_VIOLATION"
// Default TTL values
private static let DEFAULT_TTL_SECONDS: TimeInterval = 3600 // 1 hour
private static let MIN_TTL_SECONDS: TimeInterval = 60 // 1 minute
private static let MAX_TTL_SECONDS: TimeInterval = 86400 // 24 hours
// MARK: - Properties
private let database: DailyNotificationDatabase?
private let useSharedStorage: Bool
// MARK: - Initialization
/**
* Initialize TTL enforcer
*
* @param database SQLite database (nil if using UserDefaults)
* @param useSharedStorage Whether to use SQLite or UserDefaults
*/
init(database: DailyNotificationDatabase?, useSharedStorage: Bool) {
self.database = database
self.useSharedStorage = useSharedStorage
print("\(Self.TAG): TTL enforcer initialized with \(useSharedStorage ? "SQLite" : "UserDefaults")")
}
// MARK: - Freshness Validation
/**
* Check if notification content is fresh enough to arm
*
* @param slotId Notification slot ID
* @param scheduledTime T (slot time) - when notification should fire
* @param fetchedAt When content was fetched
* @return true if content is fresh enough to arm
*/
func isContentFresh(slotId: String, scheduledTime: Date, fetchedAt: Date) -> Bool {
do {
let ttlSeconds = getTTLSeconds()
// Calculate age at fire time
let ageAtFireTime = scheduledTime.timeIntervalSince(fetchedAt)
let ageAtFireSeconds = ageAtFireTime
let isFresh = ageAtFireSeconds <= ttlSeconds
if !isFresh {
logTTLViolation(slotId: slotId,
scheduledTime: scheduledTime,
fetchedAt: fetchedAt,
ageAtFireSeconds: ageAtFireSeconds,
ttlSeconds: ttlSeconds)
}
print("\(Self.TAG): TTL check for \(slotId): age=\(Int(ageAtFireSeconds))s, ttl=\(Int(ttlSeconds))s, fresh=\(isFresh)")
return isFresh
} catch {
print("\(Self.TAG): Error checking content freshness: \(error)")
// Default to allowing arming if check fails
return true
}
}
/**
* Check if notification content is fresh enough to arm (using stored fetchedAt)
*
* @param slotId Notification slot ID
* @param scheduledTime T (slot time) - when notification should fire
* @return true if content is fresh enough to arm
*/
func isContentFresh(slotId: String, scheduledTime: Date) -> Bool {
do {
guard let fetchedAt = getFetchedAt(slotId: slotId) else {
print("\(Self.TAG): No fetchedAt found for slot: \(slotId)")
return false
}
return isContentFresh(slotId: slotId, scheduledTime: scheduledTime, fetchedAt: fetchedAt)
} catch {
print("\(Self.TAG): Error checking content freshness for slot: \(slotId), error: \(error)")
return false
}
}
/**
* Validate freshness before arming notification
*
* @param notificationContent Notification content to validate
* @return true if notification should be armed
*/
func validateBeforeArming(_ notificationContent: NotificationContent) -> Bool {
do {
let slotId = notificationContent.id
let scheduledTime = Date(timeIntervalSince1970: notificationContent.scheduledTime / 1000)
let fetchedAt = Date(timeIntervalSince1970: notificationContent.fetchedAt / 1000)
print("\(Self.TAG): Validating freshness before arming: slot=\(slotId), scheduled=\(scheduledTime), fetched=\(fetchedAt)")
let isFresh = isContentFresh(slotId: slotId, scheduledTime: scheduledTime, fetchedAt: fetchedAt)
if !isFresh {
print("\(Self.TAG): Skipping arming due to TTL violation: \(slotId)")
return false
}
print("\(Self.TAG): Content is fresh, proceeding with arming: \(slotId)")
return true
} catch {
print("\(Self.TAG): Error validating freshness before arming: \(error)")
return false
}
}
// MARK: - TTL Configuration
/**
* Get TTL seconds from configuration
*
* @return TTL in seconds
*/
private func getTTLSeconds() -> TimeInterval {
do {
if useSharedStorage, let database = database {
return getTTLFromSQLite(database: database)
} else {
return getTTLFromUserDefaults()
}
} catch {
print("\(Self.TAG): Error getting TTL seconds: \(error)")
return Self.DEFAULT_TTL_SECONDS
}
}
/**
* Get TTL from SQLite database
*
* @param database SQLite database instance
* @return TTL in seconds
*/
private func getTTLFromSQLite(database: DailyNotificationDatabase) -> TimeInterval {
do {
// This would typically query the database for TTL configuration
// For now, we'll return the default value
let ttlSeconds = Self.DEFAULT_TTL_SECONDS
// Validate TTL range
let validatedTTL = max(Self.MIN_TTL_SECONDS, min(Self.MAX_TTL_SECONDS, ttlSeconds))
return validatedTTL
} catch {
print("\(Self.TAG): Error getting TTL from SQLite: \(error)")
return Self.DEFAULT_TTL_SECONDS
}
}
/**
* Get TTL from UserDefaults
*
* @return TTL in seconds
*/
private func getTTLFromUserDefaults() -> TimeInterval {
do {
let ttlSeconds = UserDefaults.standard.double(forKey: "ttlSeconds")
let finalTTL = ttlSeconds > 0 ? ttlSeconds : Self.DEFAULT_TTL_SECONDS
// Validate TTL range
let validatedTTL = max(Self.MIN_TTL_SECONDS, min(Self.MAX_TTL_SECONDS, finalTTL))
return validatedTTL
} catch {
print("\(Self.TAG): Error getting TTL from UserDefaults: \(error)")
return Self.DEFAULT_TTL_SECONDS
}
}
// MARK: - FetchedAt Retrieval
/**
* Get fetchedAt timestamp for a slot
*
* @param slotId Notification slot ID
* @return FetchedAt timestamp
*/
private func getFetchedAt(slotId: String) -> Date? {
do {
if useSharedStorage, let database = database {
return getFetchedAtFromSQLite(database: database, slotId: slotId)
} else {
return getFetchedAtFromUserDefaults(slotId: slotId)
}
} catch {
print("\(Self.TAG): Error getting fetchedAt for slot: \(slotId), error: \(error)")
return nil
}
}
/**
* Get fetchedAt from SQLite database
*
* @param database SQLite database instance
* @param slotId Notification slot ID
* @return FetchedAt timestamp
*/
private func getFetchedAtFromSQLite(database: DailyNotificationDatabase, slotId: String) -> Date? {
do {
// This would typically query the database for fetchedAt
// For now, we'll return nil
return nil
} catch {
print("\(Self.TAG): Error getting fetchedAt from SQLite: \(error)")
return nil
}
}
/**
* Get fetchedAt from UserDefaults
*
* @param slotId Notification slot ID
* @return FetchedAt timestamp
*/
private func getFetchedAtFromUserDefaults(slotId: String) -> Date? {
do {
let timestamp = UserDefaults.standard.double(forKey: "last_fetch_\(slotId)")
return timestamp > 0 ? Date(timeIntervalSince1970: timestamp / 1000) : nil
} catch {
print("\(Self.TAG): Error getting fetchedAt from UserDefaults: \(error)")
return nil
}
}
// MARK: - TTL Violation Logging
/**
* Log TTL violation with detailed information
*
* @param slotId Notification slot ID
* @param scheduledTime When notification was scheduled to fire
* @param fetchedAt When content was fetched
* @param ageAtFireSeconds Age of content at fire time
* @param ttlSeconds TTL limit in seconds
*/
private func logTTLViolation(slotId: String, scheduledTime: Date, fetchedAt: Date,
ageAtFireSeconds: TimeInterval, ttlSeconds: TimeInterval) {
do {
let violationMessage = String(format: "TTL violation: slot=%@, scheduled=%@, fetched=%@, age=%.0fs, ttl=%.0fs",
slotId, scheduledTime.description, fetchedAt.description, ageAtFireSeconds, ttlSeconds)
print("\(Self.TAG): \(Self.LOG_CODE_TTL_VIOLATION): \(violationMessage)")
// Store violation for analytics
storeTTLViolation(slotId: slotId, scheduledTime: scheduledTime, fetchedAt: fetchedAt,
ageAtFireSeconds: ageAtFireSeconds, ttlSeconds: ttlSeconds)
} catch {
print("\(Self.TAG): Error logging TTL violation: \(error)")
}
}
/**
* Store TTL violation for analytics
*/
private func storeTTLViolation(slotId: String, scheduledTime: Date, fetchedAt: Date,
ageAtFireSeconds: TimeInterval, ttlSeconds: TimeInterval) {
do {
if useSharedStorage, let database = database {
storeTTLViolationInSQLite(database: database, slotId: slotId, scheduledTime: scheduledTime,
fetchedAt: fetchedAt, ageAtFireSeconds: ageAtFireSeconds, ttlSeconds: ttlSeconds)
} else {
storeTTLViolationInUserDefaults(slotId: slotId, scheduledTime: scheduledTime, fetchedAt: fetchedAt,
ageAtFireSeconds: ageAtFireSeconds, ttlSeconds: ttlSeconds)
}
} catch {
print("\(Self.TAG): Error storing TTL violation: \(error)")
}
}
/**
* Store TTL violation in SQLite database
*/
private func storeTTLViolationInSQLite(database: DailyNotificationDatabase, slotId: String, scheduledTime: Date,
fetchedAt: Date, ageAtFireSeconds: TimeInterval, ttlSeconds: TimeInterval) {
do {
// This would typically insert into the database
// For now, we'll just log the action
print("\(Self.TAG): Storing TTL violation in SQLite for slot: \(slotId)")
} catch {
print("\(Self.TAG): Error storing TTL violation in SQLite: \(error)")
}
}
/**
* Store TTL violation in UserDefaults
*/
private func storeTTLViolationInUserDefaults(slotId: String, scheduledTime: Date, fetchedAt: Date,
ageAtFireSeconds: TimeInterval, ttlSeconds: TimeInterval) {
do {
let violationKey = "ttl_violation_\(slotId)_\(Int(scheduledTime.timeIntervalSince1970))"
let violationValue = "\(Int(fetchedAt.timeIntervalSince1970 * 1000)),\(Int(ageAtFireSeconds)),\(Int(ttlSeconds)),\(Int(Date().timeIntervalSince1970 * 1000))"
UserDefaults.standard.set(violationValue, forKey: violationKey)
} catch {
print("\(Self.TAG): Error storing TTL violation in UserDefaults: \(error)")
}
}
// MARK: - Statistics
/**
* Get TTL violation statistics
*
* @return Statistics string
*/
func getTTLViolationStats() -> String {
do {
if useSharedStorage, let database = database {
return getTTLViolationStatsFromSQLite(database: database)
} else {
return getTTLViolationStatsFromUserDefaults()
}
} catch {
print("\(Self.TAG): Error getting TTL violation stats: \(error)")
return "Error retrieving TTL violation statistics"
}
}
/**
* Get TTL violation statistics from SQLite
*/
private func getTTLViolationStatsFromSQLite(database: DailyNotificationDatabase) -> String {
do {
// This would typically query the database for violation count
// For now, we'll return a placeholder
return "TTL violations: 0"
} catch {
print("\(Self.TAG): Error getting TTL violation stats from SQLite: \(error)")
return "Error retrieving TTL violation statistics"
}
}
/**
* Get TTL violation statistics from UserDefaults
*/
private func getTTLViolationStatsFromUserDefaults() -> String {
do {
let allKeys = UserDefaults.standard.dictionaryRepresentation().keys
let violationCount = allKeys.filter { $0.hasPrefix("ttl_violation_") }.count
return "TTL violations: \(violationCount)"
} catch {
print("\(Self.TAG): Error getting TTL violation stats from UserDefaults: \(error)")
return "Error retrieving TTL violation statistics"
}
}
}