135 lines
4.9 KiB
Swift
135 lines
4.9 KiB
Swift
import Combine
|
|
import Foundation
|
|
|
|
@MainActor
|
|
public final class WatchStore: ObservableObject {
|
|
@Published public private(set) var measurements: [WatchMeasurementSummaryV1] = []
|
|
@Published public private(set) var sessions: [WatchRegulationSessionV1] = []
|
|
@Published public private(set) var outbox: [WatchSyncEnvelopeV1] = []
|
|
@Published public private(set) var tombstones: [WatchDeletionTombstoneV1] = []
|
|
@Published public var preferences: WatchPreferencesV1 {
|
|
didSet { persist() }
|
|
}
|
|
|
|
public var baseline: WatchBaselineSnapshot { .init(measurements: measurements) }
|
|
|
|
private let defaults: UserDefaults
|
|
private let storageKey: String
|
|
|
|
private struct Snapshot: Codable {
|
|
var measurements: [WatchMeasurementSummaryV1]
|
|
var sessions: [WatchRegulationSessionV1]
|
|
var outbox: [WatchSyncEnvelopeV1]
|
|
var tombstones: [WatchDeletionTombstoneV1]
|
|
var preferences: WatchPreferencesV1
|
|
}
|
|
|
|
public init(defaults: UserDefaults = .standard, storageKey: String = "haptika.watch.store.v1") {
|
|
self.defaults = defaults
|
|
self.storageKey = storageKey
|
|
if let data = defaults.data(forKey: storageKey),
|
|
let snapshot = try? WatchCoding.decoder.decode(Snapshot.self, from: data) {
|
|
measurements = snapshot.measurements
|
|
sessions = snapshot.sessions
|
|
outbox = snapshot.outbox
|
|
tombstones = snapshot.tombstones
|
|
preferences = snapshot.preferences
|
|
} else {
|
|
preferences = .init()
|
|
}
|
|
}
|
|
|
|
public func upsert(_ measurement: WatchMeasurementSummaryV1, enqueue envelope: WatchSyncEnvelopeV1? = nil) {
|
|
if let tombstone = tombstones.first(where: { $0.entityID == measurement.id }),
|
|
tombstone.updatedAt >= measurement.updatedAt { return }
|
|
measurements.removeAll { $0.id == measurement.id && $0.updatedAt <= measurement.updatedAt }
|
|
if !measurements.contains(where: { $0.id == measurement.id }) {
|
|
measurements.append(measurement)
|
|
}
|
|
measurements.sort { $0.startedAt > $1.startedAt }
|
|
measurements = Array(measurements.prefix(30))
|
|
if let envelope { enqueue(envelope) }
|
|
persist()
|
|
}
|
|
|
|
public func upsert(_ session: WatchRegulationSessionV1, enqueue envelope: WatchSyncEnvelopeV1? = nil) {
|
|
if let tombstone = tombstones.first(where: { $0.entityID == session.id }),
|
|
tombstone.updatedAt >= session.updatedAt { return }
|
|
sessions.removeAll { $0.id == session.id && $0.updatedAt <= session.updatedAt }
|
|
if !sessions.contains(where: { $0.id == session.id }) {
|
|
sessions.append(session)
|
|
}
|
|
sessions.sort { $0.startedAt > $1.startedAt }
|
|
sessions = Array(sessions.prefix(20))
|
|
if let envelope { enqueue(envelope) }
|
|
persist()
|
|
}
|
|
|
|
@discardableResult
|
|
public func deleteMeasurement(id: UUID, envelope: WatchSyncEnvelopeV1? = nil) -> WatchDeletionTombstoneV1 {
|
|
measurements.removeAll { $0.id == id }
|
|
let tombstone = WatchDeletionTombstoneV1(entityID: id, entityKind: "measurement")
|
|
tombstones.removeAll { $0.entityID == id }
|
|
tombstones.append(tombstone)
|
|
if let envelope { enqueue(envelope) }
|
|
persist()
|
|
return tombstone
|
|
}
|
|
|
|
@discardableResult
|
|
public func deleteSession(id: UUID, envelope: WatchSyncEnvelopeV1? = nil) -> WatchDeletionTombstoneV1 {
|
|
sessions.removeAll { $0.id == id }
|
|
let tombstone = WatchDeletionTombstoneV1(entityID: id, entityKind: "regulationSession")
|
|
tombstones.removeAll { $0.entityID == id }
|
|
tombstones.append(tombstone)
|
|
if let envelope { enqueue(envelope) }
|
|
persist()
|
|
return tombstone
|
|
}
|
|
|
|
public func apply(_ tombstone: WatchDeletionTombstoneV1) {
|
|
if tombstone.entityKind == "measurement" {
|
|
measurements.removeAll { $0.id == tombstone.entityID }
|
|
} else if tombstone.entityKind == "regulationSession" {
|
|
sessions.removeAll { $0.id == tombstone.entityID }
|
|
}
|
|
if let existing = tombstones.first(where: { $0.entityID == tombstone.entityID }),
|
|
existing.updatedAt >= tombstone.updatedAt { return }
|
|
tombstones.removeAll { $0.entityID == tombstone.entityID }
|
|
tombstones.append(tombstone)
|
|
persist()
|
|
}
|
|
|
|
public func enqueue(_ envelope: WatchSyncEnvelopeV1) {
|
|
guard !outbox.contains(where: { $0.eventID == envelope.eventID }) else { return }
|
|
outbox.append(envelope)
|
|
persist()
|
|
}
|
|
|
|
public func acknowledge(eventIDs: Set<UUID>) {
|
|
outbox.removeAll { eventIDs.contains($0.eventID) }
|
|
persist()
|
|
}
|
|
|
|
public func replaceFromCompanion(
|
|
measurements incomingMeasurements: [WatchMeasurementSummaryV1],
|
|
sessions incomingSessions: [WatchRegulationSessionV1]
|
|
) {
|
|
for measurement in incomingMeasurements { upsert(measurement) }
|
|
for session in incomingSessions { upsert(session) }
|
|
}
|
|
|
|
private func persist() {
|
|
let snapshot = Snapshot(
|
|
measurements: measurements,
|
|
sessions: sessions,
|
|
outbox: outbox,
|
|
tombstones: tombstones,
|
|
preferences: preferences
|
|
)
|
|
if let data = try? WatchCoding.encoder.encode(snapshot) {
|
|
defaults.set(data, forKey: storageKey)
|
|
}
|
|
}
|
|
}
|