import PhysarumEngine
/// The flat, codable mirror of a `PresetSlots` — one entry per filled slot, plus pins and seed version.
/// This is exactly what a backend writes/reads (each backend maps it to its medium: KVS keys, a JSON file).
struct SlotPayload: Codable, Equatable {
struct Entry: Codable, Equatable { var index: Int; var preset: Preset; var userOwned: Bool }
var entries: [Entry] = []
var pinned: [String] = []
var seedVersion: Int = 0
}
extension PresetSlots {
var payload: SlotPayload {
var entries: [SlotPayload.Entry] = []
for i in 0..<count where preset(at: i) != nil {
entries.append(.init(index: i, preset: preset(at: i)!, userOwned: isUserOwned(i)))
}
return SlotPayload(entries: entries, pinned: pinned.sorted(), seedVersion: seedVersion)
}
init(payload: SlotPayload) {
self.init()
for e in payload.entries { fill(index: e.index, preset: e.preset, userOwned: e.userOwned) }
applySeedVersion(payload.seedVersion)
for id in payload.pinned { pin(id) }
}
}
/// Persistence boundary. `SlotStore` talks only to this; tests inject `InMemorySlotBackend`, the app
/// uses `KVSSlotBackend` (iCloud) or `FileSlotBackend` (local).
///
/// **Load awareness** is the crux of safe iCloud sync: `NSUbiquitousKeyValueStore` downloads its data
/// ASYNCHRONOUSLY, so the first `read()` on a freshly-launched device can be empty before the cloud
/// arrives. A backend reports `isLoaded == false` until its first authoritative state is available and
/// fires `onLoaded(authoritative:)` once when it becomes available — so `SlotStore` never seeds factory
/// over (or persists factory on top of) not-yet-downloaded cloud data. `authoritative` is true when the
/// load reflects real downloaded/local state (safe to persist a first-run seed); false when it is only a
/// fallback timeout (seed in memory, don't push). Local backends are authoritative immediately.
/// `onExternalChange` fires for subsequent edits from another device. `flush()` forces a sync (the app
/// calls it on background, where iCloud-KVS pushes most reliably).
protocol SlotBackend: AnyObject {
func read() -> SlotPayload
func write(_ payload: SlotPayload)
var onExternalChange: (() -> Void)? { get set }
var isLoaded: Bool { get }
var onLoaded: ((_ authoritative: Bool) -> Void)? { get set }
func flush()
}
final class InMemorySlotBackend: SlotBackend {
private var stored = SlotPayload()
var onExternalChange: (() -> Void)?
var onLoaded: ((Bool) -> Void)?
var isLoaded: Bool { true } // a local store is authoritative immediately
func read() -> SlotPayload { stored }
func write(_ payload: SlotPayload) { stored = payload }
func flush() {}
}