SPB Git

spb/focale Public

Swift 100%
2.4 KB · 88 lines swift
Raw Blame History
1//2//  IndexScheduler.swift3//  Focale4//5//  Author: Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//8//  BGProcessingTask: on charge, screen off — never on battery9//  (CLAUDE.md §5). Incremental resume via the pipeline's checkpoint.10//1112import BackgroundTasks13import Foundation1415final class IndexScheduler: @unchecked Sendable {1617    static let shared = IndexScheduler()18    static let taskIdentifier = "ai.spboucher.focale.index"1920    private var pipeline: IndexPipeline?2122    private init() {}2324    /// Must be called before the app finishes launching.25    func register(pipeline: IndexPipeline) {26        self.pipeline = pipeline27        BGTaskScheduler.shared.register(28            forTaskWithIdentifier: Self.taskIdentifier,29            using: nil30        ) { [weak self] task in31            guard let task = task as? BGProcessingTask else { return }32            self?.handle(task)33        }34        schedule()35    }3637    func schedule() {38        let request = BGProcessingTaskRequest(identifier: Self.taskIdentifier)39        request.requiresExternalPower = true      // never on battery40        request.requiresNetworkConnectivity = false41        try? BGTaskScheduler.shared.submit(request)42    }4344    private func handle(_ task: BGProcessingTask) {45        guard let pipeline else {46            task.setTaskCompleted(success: false)47            return48        }4950        let expired = ExpirationFlag()51        task.expirationHandler = {52            expired.set()53        }5455        // Expiration flips the flag; backfill polls it between photos and56        // the checkpoint already holds our position for the next night.57        let taskBox = UncheckedSendableBox(task)58        Task {59            await pipeline.backfill { !expired.isSet }60            self.schedule()  // re-arm for the next night61            taskBox.value.setTaskCompleted(success: true)62        }63    }64}6566/// Wrapper moving a non-Sendable framework object we use serially anyway.67private struct UncheckedSendableBox<Value>: @unchecked Sendable {68    let value: Value69    init(_ value: Value) { self.value = value }70}7172/// Tiny thread-safe flag bridging the BGTask expiration callback.73private final class ExpirationFlag: @unchecked Sendable {74    private let lock = NSLock()75    private var flag = false7677    var isSet: Bool {78        lock.lock(); defer { lock.unlock() }79        return flag80    }8182    func set() {83        lock.lock()84        flag = true85        lock.unlock()86    }87}88