// // IndexScheduler.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // BGProcessingTask: on charge, screen off — never on battery // (CLAUDE.md §5). Incremental resume via the pipeline's checkpoint. // import BackgroundTasks import Foundation final class IndexScheduler: @unchecked Sendable { static let shared = IndexScheduler() static let taskIdentifier = "ai.spboucher.focale.index" private var pipeline: IndexPipeline? private init() {} /// Must be called before the app finishes launching. func register(pipeline: IndexPipeline) { self.pipeline = pipeline BGTaskScheduler.shared.register( forTaskWithIdentifier: Self.taskIdentifier, using: nil ) { [weak self] task in guard let task = task as? BGProcessingTask else { return } self?.handle(task) } schedule() } func schedule() { let request = BGProcessingTaskRequest(identifier: Self.taskIdentifier) request.requiresExternalPower = true // never on battery request.requiresNetworkConnectivity = false try? BGTaskScheduler.shared.submit(request) } private func handle(_ task: BGProcessingTask) { guard let pipeline else { task.setTaskCompleted(success: false) return } let expired = ExpirationFlag() task.expirationHandler = { expired.set() } // Expiration flips the flag; backfill polls it between photos and // the checkpoint already holds our position for the next night. let taskBox = UncheckedSendableBox(task) Task { await pipeline.backfill { !expired.isSet } self.schedule() // re-arm for the next night taskBox.value.setTaskCompleted(success: true) } } } /// Wrapper moving a non-Sendable framework object we use serially anyway. private struct UncheckedSendableBox: @unchecked Sendable { let value: Value init(_ value: Value) { self.value = value } } /// Tiny thread-safe flag bridging the BGTask expiration callback. private final class ExpirationFlag: @unchecked Sendable { private let lock = NSLock() private var flag = false var isSet: Bool { lock.lock(); defer { lock.unlock() } return flag } func set() { lock.lock() flag = true lock.unlock() } }