import BackgroundTasks import Foundation import LChatCore // 🤖 Background App Refresh: iOS periodically wakes the app (cadence is the // system's call — it learns from usage) and BackgroundSync pulls + decrypts + // files new messages into the local store off the main thread. Together with // the NSE's per-push ingest this keeps chats caught up before they're opened. // // The identifier must be listed in Info.plist's // BGTaskSchedulerPermittedIdentifiers (project.yml), and registration must // happen before didFinishLaunching returns (PushManager wires it). enum BackgroundRefresh { static var taskID: String { (Bundle.main.bundleIdentifier ?? "com.elevenmessenger.messenger") + ".refresh" } static func register() { BGTaskScheduler.shared.register(forTaskWithIdentifier: taskID, using: nil) { task in guard let refresh = task as? BGAppRefreshTask else { task.setTaskCompleted(success: false) return } handle(refresh) } } /// (Re)request the next wake. Cheap and idempotent — a duplicate submit /// for a pending request just replaces it. Called when the app backgrounds /// and from the handler itself, so the chain never breaks. static func schedule() { let req = BGAppRefreshTaskRequest(identifier: taskID) req.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) try? BGTaskScheduler.shared.submit(req) } private static func handle(_ task: BGAppRefreshTask) { schedule() // completed-once guard: the sync finishing and the expiration handler // can race; BGTaskScheduler wants exactly one completion call. let done = OneShot() let work = Task { await BackgroundSync.run(instances: InstancesStore.load()) // Release the shared store's connection BEFORE declaring done — // the process suspends any moment after, and a held app-group // file lock at suspension is the 0xdead10cc kill. await MessageStore.shared.suspend() if done.claim() { task.setTaskCompleted(success: true) } } task.expirationHandler = { work.cancel() Task { await MessageStore.shared.suspend() if done.claim() { task.setTaskCompleted(success: false) } } } } private final class OneShot: @unchecked Sendable { private let lock = NSLock() private var used = false func claim() -> Bool { lock.lock() defer { lock.unlock() } if used { return false } used = true return true } } }