Harden watch sync and App Group storage

This commit is contained in:
FuelBoard Contributor
2026-09-26 20:03:29 +01:00
parent 004fe2f787
commit c3a975eb10
5 changed files with 95 additions and 16 deletions
+39 -7
View File
@@ -419,13 +419,37 @@ struct FuelStore {
static let watchRefreshHandledKey = "fuelboard.watchRefreshHandled" // TimeInterval (seconds since 1970)
static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv"
static let stationCountKey = "fuelboard.stationCount" // String — station count
static let dataUpdatedKey = "fuelboard.dataUpdated" // String — govUK dataset update time
static let dataUpdatedKey = "fuelboard.dataUpdated" // govUK dataset update time
// Large shared payloads belong in the App Group container, not
// UserDefaults/CFPreferences (which has a roughly 4 MB limit).
static let stationsFileName = "fuelboard.stations.json"
static func appGroupFileURL(_ name: String) -> URL? {
FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupSuite)?
.appendingPathComponent(name, isDirectory: false)
}
static func loadAppGroupFile(_ name: String) -> Data? {
guard let url = appGroupFileURL(name) else { return nil }
return try? Data(contentsOf: url)
}
@discardableResult
static func saveAppGroupFile(_ data: Data, name: String) -> Bool {
guard let url = appGroupFileURL(name) else { return false }
do {
try data.write(to: url, options: .atomic)
return true
} catch {
return false
}
}
// MARK: Stations
// The full-UK dataset (~2.9 MB) lives in app-group UserDefaults only —
// keychain is for small values and cannot hold it. Read order is
// defaults-first for stations (keychain may hold a legacy small set from
// older builds; the full country dump always wins).
// The full-UK dataset is stored as an App Group file. UserDefaults is
// reserved for small preferences and has a roughly 4 MB total limit.
/// Stations from `stations` tied with `price` for `fuel` (within 0.01p),
/// sorted nearest-first from `lat`/`lng`. Used to offer multiple
@@ -444,10 +468,19 @@ struct FuelStore {
}
static func loadStations() -> [FuelStation] {
if let data = loadAppGroupFile(stationsFileName),
let stations = try? JSONDecoder().decode([FuelStation].self, from: data),
!stations.isEmpty {
return stations.map(sanitized)
}
// Migrate the legacy App Group UserDefaults value once.
if let defaults = UserDefaults(suiteName: appGroupSuite),
let data = defaults.data(forKey: stationsKey),
let stations = try? JSONDecoder().decode([FuelStation].self, from: data),
!stations.isEmpty {
if saveAppGroupFile(data, name: stationsFileName) {
defaults.removeObject(forKey: stationsKey)
}
return stations.map(sanitized)
}
if let data = keychainData(service: stationsKey),
@@ -468,8 +501,7 @@ struct FuelStore {
static func saveStations(_ stations: [FuelStation]) {
if let data = try? JSONEncoder().encode(stations) {
UserDefaults(suiteName: appGroupSuite)?.set(data, forKey: stationsKey)
// Intentionally NOT written to keychain — 2.9 MB exceeds its limits.
_ = saveAppGroupFile(data, name: stationsFileName)
}
}