Zum Inhalt

apple/PhysarumApp/Sources/Model/SlotBackend.swift

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() {}
}