From 94d025df32f4c6b8ff28c3af63a0ecd4c0b440a2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 16 Jan 2026 15:51:34 -0600 Subject: [PATCH] Update Helpers, Services Summary: - Sources: update Helpers, Services Stats: - 2 files changed, 90 insertions(+), 2 deletions(-) --- Sources/LocalData/Helpers/SyncHelper.swift | 17 ++++- .../LocalData/Services/StorageRouter.swift | 75 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/Sources/LocalData/Helpers/SyncHelper.swift b/Sources/LocalData/Helpers/SyncHelper.swift index 90bd629..daa0758 100644 --- a/Sources/LocalData/Helpers/SyncHelper.swift +++ b/Sources/LocalData/Helpers/SyncHelper.swift @@ -17,6 +17,11 @@ actor SyncHelper { public func updateConfiguration(_ configuration: SyncConfiguration) { self.configuration = configuration } + + /// Exposes the current max auto-sync size for filtering outbound payloads. + public func maxAutoSyncSize() -> Int { + configuration.maxAutoSyncSize + } // MARK: - Public Interface @@ -68,10 +73,15 @@ actor SyncHelper { guard WCSession.isSupported() else { return false } let session = WCSession.default - guard session.activationState == .activated else { return false } + guard session.activationState == .activated else { + Logger.debug("<<< [SYNC] WCSession not activated") + return false + } #if os(iOS) - return session.isPaired && session.isWatchAppInstalled + let isAvailable = session.isPaired && session.isWatchAppInstalled + Logger.debug("<<< [SYNC] WCSession status paired=\(session.isPaired) installed=\(session.isWatchAppInstalled) reachable=\(session.isReachable)") + return isAvailable #else return true #endif @@ -100,7 +110,10 @@ actor SyncHelper { guard session.isPaired, session.isWatchAppInstalled else { return } #endif + Logger.info(">>> [SYNC] Sending application context for key: \(keyName) (\(data.count) bytes)") try session.updateApplicationContext([keyName: data]) + let contextKeys = session.applicationContext.keys.sorted().joined(separator: ", ") + Logger.info("<<< [SYNC] Application context updated for key: \(keyName). Keys now: [\(contextKeys)]") } private func setupSession() { diff --git a/Sources/LocalData/Services/StorageRouter.swift b/Sources/LocalData/Services/StorageRouter.swift index d0cfc99..d455746 100644 --- a/Sources/LocalData/Services/StorageRouter.swift +++ b/Sources/LocalData/Services/StorageRouter.swift @@ -553,6 +553,81 @@ public actor StorageRouter: StorageProviding { ) } + /// Attempts to sync any registered keys that already have stored values. + /// This is useful for bootstrapping watch data after app launch or reconnection. + public func syncRegisteredKeysIfNeeded() async { + let isAvailable = await sync.isSyncAvailable() + guard isAvailable else { + Logger.debug("<<< [SYNC] Skipping bootstrap sync: WatchConnectivity unavailable") + return + } + + Logger.debug(">>> [SYNC] Starting bootstrap sync for registered keys") + for entry in registeredKeys.values { + let descriptor = entry.descriptor + guard descriptor.availability == .all || descriptor.availability == .phoneWithWatchSync else { + Logger.debug("<<< [SYNC] Skipping key \(descriptor.name): availability=\(descriptor.availability)") + continue + } + guard descriptor.syncPolicy != .never else { + Logger.debug("<<< [SYNC] Skipping key \(descriptor.name): syncPolicy=\(descriptor.syncPolicy)") + continue + } + + do { + guard let storedData = try await retrieve(for: descriptor) else { + Logger.debug("<<< [SYNC] No stored data for key \(descriptor.name)") + continue + } + try await sync.syncIfNeeded( + data: storedData, + keyName: descriptor.name, + availability: descriptor.availability, + syncPolicy: descriptor.syncPolicy + ) + Logger.debug("<<< [SYNC] Bootstrapped context for key: \(descriptor.name) (\(storedData.count) bytes)") + } catch { + Logger.error("Failed to bootstrap sync for key: \(descriptor.name)", error: error) + } + } + } + + /// Builds a snapshot of syncable key data for immediate watch requests. + public func syncSnapshot() async -> [String: Data] { + let isAvailable = await sync.isSyncAvailable() + guard isAvailable else { + Logger.debug("<<< [SYNC] Skipping snapshot: WatchConnectivity unavailable") + return [:] + } + + let maxAutoSyncSize = await sync.maxAutoSyncSize() + var payload: [String: Data] = [:] + + for entry in registeredKeys.values { + let descriptor = entry.descriptor + guard descriptor.availability == .all || descriptor.availability == .phoneWithWatchSync else { + continue + } + guard descriptor.syncPolicy != .never else { continue } + + do { + guard let storedData = try await retrieve(for: descriptor) else { continue } + + if descriptor.syncPolicy == .automaticSmall, storedData.count > maxAutoSyncSize { + Logger.debug("<<< [SYNC] Snapshot skip \(descriptor.name): size exceeds maxAutoSyncSize") + continue + } + + payload[descriptor.name] = storedData + } catch { + Logger.error("Failed to build sync snapshot for key: \(descriptor.name)", error: error) + } + } + + Logger.debug("<<< [SYNC] Snapshot built with \(payload.count) keys") + return payload + } + // MARK: - Internal Sync Handling /// Internal method to update storage from received sync data.