spb/focale Public
Swift 100%
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