@sriinnu/runic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swiftformat +50 -0
- package/.swiftlint.yml +160 -0
- package/CHANGELOG.md +307 -0
- package/CONTRIBUTING.md +456 -0
- package/Icon.icns +0 -0
- package/Icon.icon/Assets/runic.png +0 -0
- package/Icon.icon/icon.json +36 -0
- package/LICENSE +21 -0
- package/Package.resolved +51 -0
- package/Package.swift +136 -0
- package/Packages/Helix/LICENSE +21 -0
- package/Packages/Helix/Package.swift +29 -0
- package/Packages/Helix/README.md +976 -0
- package/Packages/Helix/Sources/Helix/Binding.swift +111 -0
- package/Packages/Helix/Sources/Helix/CommandParser.swift +160 -0
- package/Packages/Helix/Sources/Helix/CommandSignature.swift +117 -0
- package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +72 -0
- package/Packages/Helix/Sources/Helix/HelpGenerator.swift +175 -0
- package/Packages/Helix/Sources/Helix/NameSpecification.swift +98 -0
- package/Packages/Helix/Sources/Helix/ParsableCommand.swift +204 -0
- package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +44 -0
- package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +34 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +91 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +94 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +167 -0
- package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +99 -0
- package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +146 -0
- package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +97 -0
- package/Packages/Helix/Sources/Helix/Program.swift +204 -0
- package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +367 -0
- package/Packages/Silo/LICENSE +21 -0
- package/Packages/Silo/Package.swift +34 -0
- package/Packages/Silo/README.md +466 -0
- package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +169 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +365 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +594 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +326 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +154 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +119 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +121 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +391 -0
- package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +240 -0
- package/Packages/Silo/Sources/Silo/CryptoSupport.swift +86 -0
- package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +172 -0
- package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +172 -0
- package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +105 -0
- package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +191 -0
- package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +41 -0
- package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +110 -0
- package/README.md +109 -0
- package/RuniciOS/App/RuniciOSApp.swift +105 -0
- package/RuniciOS/Core/iOSUsageStore.swift +192 -0
- package/RuniciOS/Views/ProviderDetailView.swift +312 -0
- package/RuniciOS/Views/ProviderListView.swift +228 -0
- package/RuniciOS/Widgets/LargeWidget.swift +367 -0
- package/RuniciOS/Widgets/LiveActivity.swift +342 -0
- package/RuniciOS/Widgets/LockScreenWidget.swift +273 -0
- package/RuniciOS/Widgets/MediumWidget.swift +370 -0
- package/RuniciOS/Widgets/RunicWidget.swift +274 -0
- package/RuniciOS/Widgets/SmallWidget.swift +281 -0
- package/RuniciOS/Widgets/WidgetModels.swift +285 -0
- package/RuniciOS/Widgets/WidgetTimeline.swift +270 -0
- package/Scripts/CREATION-SUMMARY.md +367 -0
- package/Scripts/DIRECTORY-STRUCTURE.md +358 -0
- package/Scripts/GETTING-STARTED-CHECKLIST.md +364 -0
- package/Scripts/INDEX.md +365 -0
- package/Scripts/QUICK-REFERENCE.md +164 -0
- package/Scripts/README-SCRIPTS.md +926 -0
- package/Scripts/build-android.sh +526 -0
- package/Scripts/build-ios.sh +529 -0
- package/Scripts/build-macos.sh +603 -0
- package/Scripts/build-windows.sh +504 -0
- package/Scripts/build_icon.sh +49 -0
- package/Scripts/changelog-to-html.sh +89 -0
- package/Scripts/check-release-assets.sh +10 -0
- package/Scripts/ci-config.yml +474 -0
- package/Scripts/compile_and_run.sh +219 -0
- package/Scripts/compile_and_run_adhoc.sh +4 -0
- package/Scripts/docs-list.mjs +119 -0
- package/Scripts/make_appcast.sh +58 -0
- package/Scripts/package_app.sh +409 -0
- package/Scripts/release-lib.sh +214 -0
- package/Scripts/release.sh +76 -0
- package/Scripts/setup-all.sh +387 -0
- package/Scripts/setup-ios.sh +404 -0
- package/Scripts/setup-react-native.sh +523 -0
- package/Scripts/sign-and-notarize.sh +122 -0
- package/Scripts/test_live_update.sh +38 -0
- package/Scripts/validate_changelog.sh +23 -0
- package/Scripts/verify_appcast.sh +92 -0
- package/Sources/Runic/About.swift +93 -0
- package/Sources/Runic/AppNotifications.swift +115 -0
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +657 -0
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +518 -0
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +810 -0
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +87 -0
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +1379 -0
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +337 -0
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +395 -0
- package/Sources/Runic/Controllers/StatusItemController.swift +360 -0
- package/Sources/Runic/Core/BudgetNotificationManager.swift +80 -0
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +103 -0
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +154 -0
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +206 -0
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +95 -0
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +1216 -0
- package/Sources/Runic/Core/Rendering/IconView.swift +120 -0
- package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +71 -0
- package/Sources/Runic/Core/RunicAnimation.swift +53 -0
- package/Sources/Runic/Core/RunicColors.swift +73 -0
- package/Sources/Runic/Core/RunicCornerRadius.swift +15 -0
- package/Sources/Runic/Core/RunicSpacing.swift +68 -0
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +158 -0
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +30 -0
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/SettingsStore.swift +1702 -0
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +20 -0
- package/Sources/Runic/Core/Stores/UsageStore+Status.swift +189 -0
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +33 -0
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +67 -0
- package/Sources/Runic/Core/Stores/UsageStore.swift +3564 -0
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +143 -0
- package/Sources/Runic/Core/UsageExporter.swift +193 -0
- package/Sources/Runic/Date+RelativeDescription.swift +21 -0
- package/Sources/Runic/HiddenWindowView.swift +32 -0
- package/Sources/Runic/InstallOrigin.swift +9 -0
- package/Sources/Runic/KeyboardShortcuts+Names.swift +6 -0
- package/Sources/Runic/MouseLocationReader.swift +65 -0
- package/Sources/Runic/Notifications+Runic.swift +6 -0
- package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +503 -0
- package/Sources/Runic/ProviderRegistry.swift +100 -0
- package/Sources/Runic/ProviderSwitcherButtons.swift +267 -0
- package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +11 -0
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +14 -0
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +52 -0
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +42 -0
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +26 -0
- package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +14 -0
- package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +18 -0
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +193 -0
- package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +110 -0
- package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +28 -0
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +12 -0
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +20 -0
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +32 -0
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +15 -0
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +48 -0
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +66 -0
- package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +12 -0
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +99 -0
- package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +18 -0
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +24 -0
- package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +2 -0
- package/Sources/Runic/Resources/ProviderIcon-auggie.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-azure.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +7 -0
- package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-claude.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-codex.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-cohere.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-copilot.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-cursor.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-factory.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-gemini.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-groq.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-kimi.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-minimax.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-mistral.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +7 -0
- package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-qwen.svg +9 -0
- package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-together.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +17 -0
- package/Sources/Runic/Resources/ProviderIcon-xai.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-zai.svg +5 -0
- package/Sources/Runic/Resources/RunicMenubarIcon.svg +7 -0
- package/Sources/Runic/RunicApp.swift +357 -0
- package/Sources/Runic/SessionQuotaNotifications.swift +58 -0
- package/Sources/Runic/UsagePaceText.swift +52 -0
- package/Sources/Runic/UsageProgressBar.swift +224 -0
- package/Sources/Runic/Utilities/DisplayLink.swift +96 -0
- package/Sources/Runic/Utilities/LoadingPattern.swift +60 -0
- package/Sources/Runic/Utilities/PerformanceConstants.swift +85 -0
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +256 -0
- package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +22 -0
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +275 -0
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +377 -0
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +312 -0
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +312 -0
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +228 -0
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +228 -0
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +162 -0
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +64 -0
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +268 -0
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +104 -0
- package/Sources/Runic/Views/Menu/MenuCardView.swift +1900 -0
- package/Sources/Runic/Views/Menu/MenuContent.swift +150 -0
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +369 -0
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +42 -0
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +235 -0
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +141 -0
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +209 -0
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +220 -0
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +88 -0
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +201 -0
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +398 -0
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +232 -0
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +417 -0
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +190 -0
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +132 -0
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +618 -0
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +305 -0
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +271 -0
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +268 -0
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +422 -0
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +641 -0
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +344 -0
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +230 -0
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +322 -0
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +469 -0
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +231 -0
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +50 -0
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +497 -0
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +288 -0
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +3435 -0
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +25 -0
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +59 -0
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +206 -0
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +238 -0
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +371 -0
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +175 -0
- package/Sources/Runic/Views/Team/TeamManagementView.swift +761 -0
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +181 -0
- package/Sources/RunicCLI/CLIEntry.swift +942 -0
- package/Sources/RunicCLI/CLIRenderer.swift +87 -0
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +588 -0
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +362 -0
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +439 -0
- package/Sources/RunicCLI/Commands/ResetCommand.swift +429 -0
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +168 -0
- package/Sources/RunicClaudeWatchdog/main.swift +122 -0
- package/Sources/RunicClaudeWebProbe/main.swift +62 -0
- package/Sources/RunicCore/AlertRuleStore.swift +187 -0
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +25 -0
- package/Sources/RunicCore/CopilotUsageModels.swift +84 -0
- package/Sources/RunicCore/CostUsageFetcher.swift +119 -0
- package/Sources/RunicCore/CostUsageModels.swift +479 -0
- package/Sources/RunicCore/CreditsModels.swift +50 -0
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +269 -0
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +622 -0
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +500 -0
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +679 -0
- package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +135 -0
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +173 -0
- package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +79 -0
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +86 -0
- package/Sources/RunicCore/Logging/RunicLog.swift +123 -0
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +371 -0
- package/Sources/RunicCore/OpenAIDashboardModels.swift +210 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +676 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +492 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +51 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +483 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +558 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +204 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +78 -0
- package/Sources/RunicCore/PathEnvironment.swift +482 -0
- package/Sources/RunicCore/Performance/PerformanceModels.swift +371 -0
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +409 -0
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +178 -0
- package/Sources/RunicCore/ProjectBudgetStore.swift +99 -0
- package/Sources/RunicCore/ProviderCostSnapshot.swift +29 -0
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +66 -0
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +685 -0
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +142 -0
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +141 -0
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +186 -0
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +126 -0
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +168 -0
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +332 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +180 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +137 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +207 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +654 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +28 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +480 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +780 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +146 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +269 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +100 -0
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +243 -0
- package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +125 -0
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +24 -0
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +181 -0
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +85 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +123 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +114 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +100 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +291 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +386 -0
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +594 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +85 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +108 -0
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +398 -0
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1275 -0
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +835 -0
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +71 -0
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +77 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +92 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +54 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +171 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +911 -0
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +87 -0
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +207 -0
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/ProviderBranding.swift +25 -0
- package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +17 -0
- package/Sources/RunicCore/Providers/ProviderDescriptor.swift +129 -0
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +263 -0
- package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +98 -0
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +501 -0
- package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +68 -0
- package/Sources/RunicCore/Providers/Providers.swift +184 -0
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +30 -0
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +317 -0
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +123 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +48 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +144 -0
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +72 -0
- package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +41 -0
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +724 -0
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +385 -0
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +310 -0
- package/Sources/RunicCore/Sync/SyncProtocol.swift +288 -0
- package/Sources/RunicCore/Sync/SyncRecord.swift +405 -0
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +294 -0
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +775 -0
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +333 -0
- package/Sources/RunicCore/Team/TeamModels.swift +583 -0
- package/Sources/RunicCore/Team/TeamShowbackExporter.swift +495 -0
- package/Sources/RunicCore/Team/TeamStore.swift +792 -0
- package/Sources/RunicCore/TextParsing.swift +55 -0
- package/Sources/RunicCore/UsageFetcher.swift +694 -0
- package/Sources/RunicCore/UsageFormatter.swift +410 -0
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +329 -0
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +448 -0
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +550 -0
- package/Sources/RunicCore/UsageLedger/UsageLedger.swift +29 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +660 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +209 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +417 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +245 -0
- package/Sources/RunicCore/UsagePace.swift +81 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +73 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +74 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +208 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +455 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +132 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +589 -0
- package/Sources/RunicCore/WidgetSnapshot.swift +183 -0
- package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +14 -0
- package/Sources/RunicMacros/ProviderRegistrationMacros.swift +216 -0
- package/Sources/RunicWidget/RunicWidgetBundle.swift +79 -0
- package/Sources/RunicWidget/RunicWidgetProvider.swift +283 -0
- package/Sources/RunicWidget/RunicWidgetViews.swift +680 -0
- package/Tests/RunicTests/AntigravityStatusProbeTests.swift +50 -0
- package/Tests/RunicTests/AppDelegateTests.swift +43 -0
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +22 -0
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +70 -0
- package/Tests/RunicTests/CLICostTests.swift +65 -0
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +22 -0
- package/Tests/RunicTests/CLISnapshotTests.swift +72 -0
- package/Tests/RunicTests/CLIWebFallbackTests.swift +58 -0
- package/Tests/RunicTests/ClaudeOAuthTests.swift +164 -0
- package/Tests/RunicTests/ClaudeResilienceTests.swift +30 -0
- package/Tests/RunicTests/ClaudeUsageTests.swift +370 -0
- package/Tests/RunicTests/CodexOAuthTests.swift +122 -0
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +82 -0
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +75 -0
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +75 -0
- package/Tests/RunicTests/CostUsageDecodingTests.swift +394 -0
- package/Tests/RunicTests/CostUsagePricingTests.swift +50 -0
- package/Tests/RunicTests/CostUsageScannerTests.swift +700 -0
- package/Tests/RunicTests/CursorStatusProbeTests.swift +323 -0
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +69 -0
- package/Tests/RunicTests/GeminiAPITestHelpers.swift +94 -0
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +28 -0
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +283 -0
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +279 -0
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +233 -0
- package/Tests/RunicTests/GeminiTestEnvironment.swift +87 -0
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +73 -0
- package/Tests/RunicTests/InstallOriginTests.swift +18 -0
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +9 -0
- package/Tests/RunicTests/LiveAccountTests.swift +24 -0
- package/Tests/RunicTests/LoadingPatternTests.swift +35 -0
- package/Tests/RunicTests/MenuCardModelTests.swift +545 -0
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/MiniMaxParsingTests.swift +68 -0
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +112 -0
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +17 -0
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +75 -0
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +27 -0
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +134 -0
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +53 -0
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +92 -0
- package/Tests/RunicTests/PathBuilderTests.swift +237 -0
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +160 -0
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +38 -0
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +16 -0
- package/Tests/RunicTests/ProviderRegistryTests.swift +29 -0
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +202 -0
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +51 -0
- package/Tests/RunicTests/RunicTests.swift +120 -0
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +36 -0
- package/Tests/RunicTests/SettingsStoreTests.swift +161 -0
- package/Tests/RunicTests/StatusItemAnimationTests.swift +49 -0
- package/Tests/RunicTests/StatusMenuTests.swift +434 -0
- package/Tests/RunicTests/StatusProbeTests.swift +377 -0
- package/Tests/RunicTests/SubprocessRunnerTests.swift +20 -0
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +65 -0
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +148 -0
- package/Tests/RunicTests/TTYIntegrationTests.swift +41 -0
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +159 -0
- package/Tests/RunicTests/TextParsingTests.swift +12 -0
- package/Tests/RunicTests/UsageFormatterTests.swift +112 -0
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +172 -0
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +213 -0
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +83 -0
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +170 -0
- package/Tests/RunicTests/UsagePaceTests.swift +77 -0
- package/Tests/RunicTests/UsagePaceTextTests.swift +109 -0
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +91 -0
- package/Tests/RunicTests/WidgetSnapshotTests.swift +45 -0
- package/Tests/RunicTests/ZaiProviderTests.swift +61 -0
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +6 -0
- package/TestsLinux/PlatformGatingTests.swift +46 -0
- package/appcast.xml +105 -0
- package/assets/logo-concept-stack.svg +24 -0
- package/assets/logo-minimal-bars.svg +15 -0
- package/assets/logo.svg +20 -0
- package/assets/logo2.svg +43 -0
- package/bin/docs-list +5 -0
- package/bin/install-runic-cli.sh +32 -0
- package/build-and-run.sh +369 -0
- package/docs/.nojekyll +1 -0
- package/docs/CNAME +1 -0
- package/docs/antigravity.md +75 -0
- package/docs/architecture.md +32 -0
- package/docs/claude.md +93 -0
- package/docs/cli-commands/integration_guide.md +371 -0
- package/docs/cli-commands/quick_reference.md +362 -0
- package/docs/cli-commands/readme.md +361 -0
- package/docs/cli.md +156 -0
- package/docs/cloudkit_sync_guide.md +495 -0
- package/docs/codebase_review.md +288 -0
- package/docs/codex-oauth.md +635 -0
- package/docs/codex.md +104 -0
- package/docs/codexbar.png +0 -0
- package/docs/copilot.md +44 -0
- package/docs/cursor.md +46 -0
- package/docs/design/designcomparisonchart.md +510 -0
- package/docs/design/implementationguide.md +792 -0
- package/docs/design/menubaricondesignconcepts.md +796 -0
- package/docs/design/mockups/hybridiconconcept.svg +62 -0
- package/docs/design/mockups/infinityiconconcept.svg +41 -0
- package/docs/design/mockups/infinityiconstates.svg +129 -0
- package/docs/design/mockups/wavelogorefined.svg +48 -0
- package/docs/design/quick_summary.md +426 -0
- package/docs/design/readme.md +478 -0
- package/docs/design/sizespecifications.md +575 -0
- package/docs/design-system.md +836 -0
- package/docs/factory.md +103 -0
- package/docs/gemini.md +73 -0
- package/docs/icon.md +37 -0
- package/docs/icon.png +0 -0
- package/docs/index.html +175 -0
- package/docs/keychain_fix.md +274 -0
- package/docs/minimax.md +71 -0
- package/docs/packaging.md +25 -0
- package/docs/progress-bar-improvements.md +240 -0
- package/docs/progress-bar-quick-reference.md +283 -0
- package/docs/progress-bar-technical-details.md +432 -0
- package/docs/progress-bar-visual-test.swift +375 -0
- package/docs/provider.md +202 -0
- package/docs/providers.md +86 -0
- package/docs/refactor/macros.md +35 -0
- package/docs/refresh-loop.md +22 -0
- package/docs/releasing-homebrew.md +35 -0
- package/docs/releasing.md +121 -0
- package/docs/research/concrete_todo_2026-02-22.md +47 -0
- package/docs/research/core-feature-research.md +248 -0
- package/docs/research/feature_uiux_roadmap_2026-02-22.md +202 -0
- package/docs/research/parity_matrix.md +49 -0
- package/docs/research/provider_expansion_candidates_2026-02-22.md +80 -0
- package/docs/research/provider_model_token_project_matrix_2026-02-23.md +46 -0
- package/docs/research/release_gate_checklist.md +57 -0
- package/docs/site.css +505 -0
- package/docs/sparkle.md +28 -0
- package/docs/status.md +28 -0
- package/docs/sync/implementation.md +393 -0
- package/docs/sync/readme.md +317 -0
- package/docs/sync_files_summary.md +391 -0
- package/docs/sync_quick_reference.md +225 -0
- package/docs/todo.md +11 -0
- package/docs/ui.md +29 -0
- package/docs/web-integration.md +9 -0
- package/docs/widgets.md +85 -0
- package/docs/workflows/error-handling-improvements.md +434 -0
- package/docs/workflows/folder_structure.md +299 -0
- package/docs/workflows/loading_states.md +446 -0
- package/docs/workflows/loading_states_examples.md +565 -0
- package/docs/workflows/loading_states_flow.md +430 -0
- package/docs/workflows/performance_optimizations.md +457 -0
- package/docs/workflows/phase1-cli-testing-report.md +313 -0
- package/docs/workflows/quick_reference.md +228 -0
- package/docs/workflows/readme.md +59 -0
- package/docs/workflows/recent_improvements.md +275 -0
- package/docs/workflows/sync_enablement_summary.md +400 -0
- package/docs/zai.md +43 -0
- package/package.json +36 -0
- package/runic.png +0 -0
- package/version.env +2 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Combine
|
|
3
|
+
|
|
4
|
+
#if canImport(UIKit)
|
|
5
|
+
import UIKit
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
#if canImport(AppKit)
|
|
9
|
+
import AppKit
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
// MARK: - Background Sync Manager
|
|
13
|
+
|
|
14
|
+
/// Manages background synchronization with configurable intervals
|
|
15
|
+
///
|
|
16
|
+
/// This manager coordinates automatic synchronization operations based on
|
|
17
|
+
/// app lifecycle events and configured intervals. It supports:
|
|
18
|
+
/// - Active sync (5-minute intervals)
|
|
19
|
+
/// - Background sync (hourly intervals)
|
|
20
|
+
/// - Manual sync on demand
|
|
21
|
+
/// - Lifecycle-aware sync (app launch, foreground, background)
|
|
22
|
+
/// - Network reachability monitoring
|
|
23
|
+
public actor BackgroundSyncManager {
|
|
24
|
+
|
|
25
|
+
// MARK: - Properties
|
|
26
|
+
|
|
27
|
+
/// The underlying sync engine
|
|
28
|
+
private let syncEngine: any SyncEngine
|
|
29
|
+
|
|
30
|
+
/// Sync configuration
|
|
31
|
+
private let config: SyncConfiguration
|
|
32
|
+
|
|
33
|
+
/// Timer for periodic sync operations (must be accessed on main thread)
|
|
34
|
+
@MainActor
|
|
35
|
+
private var syncTimer: Timer?
|
|
36
|
+
|
|
37
|
+
/// Indicates if background sync is currently enabled
|
|
38
|
+
private var isEnabled: Bool = false
|
|
39
|
+
|
|
40
|
+
/// Last successful sync timestamp
|
|
41
|
+
private var lastSyncDate: Date?
|
|
42
|
+
|
|
43
|
+
/// Sync history for diagnostics
|
|
44
|
+
private var syncHistory: [SyncHistoryEntry] = []
|
|
45
|
+
|
|
46
|
+
/// Maximum history entries to retain
|
|
47
|
+
private let maxHistoryEntries: Int = 50
|
|
48
|
+
|
|
49
|
+
/// Observers registered for sync events
|
|
50
|
+
private var observers: [WeakSyncObserver] = []
|
|
51
|
+
|
|
52
|
+
/// Background task identifier (iOS only)
|
|
53
|
+
#if canImport(UIKit)
|
|
54
|
+
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
|
55
|
+
#endif
|
|
56
|
+
|
|
57
|
+
// MARK: - Initialization
|
|
58
|
+
|
|
59
|
+
/// Initializes the background sync manager
|
|
60
|
+
///
|
|
61
|
+
/// - Parameters:
|
|
62
|
+
/// - syncEngine: The sync engine to use for operations
|
|
63
|
+
/// - config: Configuration for sync intervals and behavior
|
|
64
|
+
public init(
|
|
65
|
+
syncEngine: any SyncEngine,
|
|
66
|
+
config: SyncConfiguration = .default
|
|
67
|
+
) {
|
|
68
|
+
self.syncEngine = syncEngine
|
|
69
|
+
self.config = config
|
|
70
|
+
|
|
71
|
+
// Register for app lifecycle notifications
|
|
72
|
+
Task {
|
|
73
|
+
await registerLifecycleObservers()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Public Methods
|
|
78
|
+
|
|
79
|
+
/// Starts background synchronization
|
|
80
|
+
public func start() {
|
|
81
|
+
guard !isEnabled else { return }
|
|
82
|
+
|
|
83
|
+
isEnabled = true
|
|
84
|
+
scheduleNextSync()
|
|
85
|
+
notifyObservers { $0.syncDidStart() }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Stops background synchronization
|
|
89
|
+
public func stop() {
|
|
90
|
+
isEnabled = false
|
|
91
|
+
Task { @MainActor in
|
|
92
|
+
syncTimer?.invalidate()
|
|
93
|
+
syncTimer = nil
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Performs a manual sync operation
|
|
98
|
+
///
|
|
99
|
+
/// - Parameter options: Optional sync options (uses defaults if nil)
|
|
100
|
+
/// - Returns: Result of the sync operation
|
|
101
|
+
@discardableResult
|
|
102
|
+
public func syncNow(options: SyncOptions? = nil) async -> Result<SyncResult, SyncError> {
|
|
103
|
+
let syncOptions = options ?? config.defaultSyncOptions
|
|
104
|
+
|
|
105
|
+
notifyObservers { $0.syncDidStart() }
|
|
106
|
+
|
|
107
|
+
let result = await syncEngine.sync(options: syncOptions)
|
|
108
|
+
|
|
109
|
+
switch result {
|
|
110
|
+
case .success(let syncResult):
|
|
111
|
+
lastSyncDate = Date()
|
|
112
|
+
recordSyncHistory(success: true, result: syncResult)
|
|
113
|
+
notifyObservers { $0.syncDidComplete(result: syncResult) }
|
|
114
|
+
|
|
115
|
+
case .failure(let error):
|
|
116
|
+
recordSyncHistory(success: false, error: error)
|
|
117
|
+
notifyObservers { $0.syncDidFail(error: error) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Adds an observer for sync events
|
|
124
|
+
///
|
|
125
|
+
/// - Parameter observer: The observer to add
|
|
126
|
+
public func addObserver(_ observer: SyncObserver) {
|
|
127
|
+
let weakObserver = WeakSyncObserver(observer)
|
|
128
|
+
observers.append(weakObserver)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Removes an observer
|
|
132
|
+
///
|
|
133
|
+
/// - Parameter observer: The observer to remove
|
|
134
|
+
public func removeObserver(_ observer: SyncObserver) {
|
|
135
|
+
observers.removeAll { $0.observer === observer }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Returns sync statistics
|
|
139
|
+
public var statistics: SyncStatistics {
|
|
140
|
+
SyncStatistics(
|
|
141
|
+
lastSyncDate: lastSyncDate,
|
|
142
|
+
totalSyncs: syncHistory.count,
|
|
143
|
+
successfulSyncs: syncHistory.filter { $0.success }.count,
|
|
144
|
+
failedSyncs: syncHistory.filter { !$0.success }.count,
|
|
145
|
+
averageDuration: calculateAverageDuration(),
|
|
146
|
+
syncHistory: Array(syncHistory.suffix(10))
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Updates sync configuration
|
|
151
|
+
///
|
|
152
|
+
/// - Parameter config: New configuration to apply
|
|
153
|
+
public func updateConfiguration(_ config: SyncConfiguration) async {
|
|
154
|
+
// Note: Configuration updates require recreating the manager
|
|
155
|
+
// This is a placeholder - in practice, you'd want to make config mutable
|
|
156
|
+
if isEnabled {
|
|
157
|
+
stop()
|
|
158
|
+
// Caller should create a new BackgroundSyncManager with new config
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - Private Methods
|
|
163
|
+
|
|
164
|
+
/// Schedules the next automatic sync
|
|
165
|
+
private func scheduleNextSync() {
|
|
166
|
+
guard isEnabled else { return }
|
|
167
|
+
|
|
168
|
+
let interval = determineNextInterval()
|
|
169
|
+
|
|
170
|
+
Task { @MainActor [weak self] in
|
|
171
|
+
guard let self = self else { return }
|
|
172
|
+
self.syncTimer?.invalidate()
|
|
173
|
+
self.syncTimer = Timer.scheduledTimer(
|
|
174
|
+
withTimeInterval: interval,
|
|
175
|
+
repeats: false
|
|
176
|
+
) { [weak self] _ in
|
|
177
|
+
Task {
|
|
178
|
+
await self?.performScheduledSync()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Determines the appropriate sync interval based on app state
|
|
185
|
+
private func determineNextInterval() -> TimeInterval {
|
|
186
|
+
#if canImport(UIKit)
|
|
187
|
+
let isActive = UIApplication.shared.applicationState == .active
|
|
188
|
+
return isActive ? config.activeSyncInterval : config.backgroundSyncInterval
|
|
189
|
+
#else
|
|
190
|
+
return config.activeSyncInterval
|
|
191
|
+
#endif
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Performs a scheduled background sync
|
|
195
|
+
private func performScheduledSync() async {
|
|
196
|
+
#if canImport(UIKit)
|
|
197
|
+
// Begin background task
|
|
198
|
+
backgroundTaskID = UIApplication.shared.beginBackgroundTask { [weak self] in
|
|
199
|
+
self?.endBackgroundTask()
|
|
200
|
+
}
|
|
201
|
+
#endif
|
|
202
|
+
|
|
203
|
+
await syncNow()
|
|
204
|
+
|
|
205
|
+
#if canImport(UIKit)
|
|
206
|
+
endBackgroundTask()
|
|
207
|
+
#endif
|
|
208
|
+
|
|
209
|
+
// Schedule next sync
|
|
210
|
+
scheduleNextSync()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#if canImport(UIKit)
|
|
214
|
+
/// Ends the background task
|
|
215
|
+
private func endBackgroundTask() {
|
|
216
|
+
guard backgroundTaskID != .invalid else { return }
|
|
217
|
+
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
|
218
|
+
backgroundTaskID = .invalid
|
|
219
|
+
}
|
|
220
|
+
#endif
|
|
221
|
+
|
|
222
|
+
/// Registers observers for app lifecycle events
|
|
223
|
+
private func registerLifecycleObservers() {
|
|
224
|
+
#if canImport(UIKit)
|
|
225
|
+
NotificationCenter.default.addObserver(
|
|
226
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
227
|
+
object: nil,
|
|
228
|
+
queue: .main
|
|
229
|
+
) { [weak self] _ in
|
|
230
|
+
Task { await self?.handleAppBecameActive() }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
NotificationCenter.default.addObserver(
|
|
234
|
+
forName: UIApplication.didEnterBackgroundNotification,
|
|
235
|
+
object: nil,
|
|
236
|
+
queue: .main
|
|
237
|
+
) { [weak self] _ in
|
|
238
|
+
Task { await self?.handleAppEnteredBackground() }
|
|
239
|
+
}
|
|
240
|
+
#endif
|
|
241
|
+
|
|
242
|
+
#if canImport(AppKit)
|
|
243
|
+
NotificationCenter.default.addObserver(
|
|
244
|
+
forName: NSApplication.didBecomeActiveNotification,
|
|
245
|
+
object: nil,
|
|
246
|
+
queue: .main
|
|
247
|
+
) { [weak self] _ in
|
|
248
|
+
Task { await self?.handleAppBecameActive() }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
NotificationCenter.default.addObserver(
|
|
252
|
+
forName: NSApplication.didResignActiveNotification,
|
|
253
|
+
object: nil,
|
|
254
|
+
queue: .main
|
|
255
|
+
) { [weak self] _ in
|
|
256
|
+
Task { await self?.handleAppEnteredBackground() }
|
|
257
|
+
}
|
|
258
|
+
#endif
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Handles app becoming active
|
|
262
|
+
private func handleAppBecameActive() async {
|
|
263
|
+
guard config.syncOnForeground else { return }
|
|
264
|
+
|
|
265
|
+
// Sync if last sync was more than interval ago
|
|
266
|
+
if shouldSyncOnForeground() {
|
|
267
|
+
await syncNow()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Handles app entering background
|
|
272
|
+
private func handleAppEnteredBackground() async {
|
|
273
|
+
guard config.syncOnBackground else { return }
|
|
274
|
+
await syncNow()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Determines if sync should occur on foreground
|
|
278
|
+
private func shouldSyncOnForeground() -> Bool {
|
|
279
|
+
guard let lastSync = lastSyncDate else { return true }
|
|
280
|
+
|
|
281
|
+
let elapsed = Date().timeIntervalSince(lastSync)
|
|
282
|
+
return elapsed >= config.activeSyncInterval
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Records sync operation in history
|
|
286
|
+
private func recordSyncHistory(
|
|
287
|
+
success: Bool,
|
|
288
|
+
result: SyncResult? = nil,
|
|
289
|
+
error: SyncError? = nil
|
|
290
|
+
) {
|
|
291
|
+
let entry = SyncHistoryEntry(
|
|
292
|
+
timestamp: Date(),
|
|
293
|
+
success: success,
|
|
294
|
+
duration: result?.duration ?? 0,
|
|
295
|
+
pushedCount: result?.pushedCount ?? 0,
|
|
296
|
+
fetchedCount: result?.fetchedCount ?? 0,
|
|
297
|
+
error: error?.localizedDescription
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
syncHistory.append(entry)
|
|
301
|
+
|
|
302
|
+
// Trim history if needed
|
|
303
|
+
if syncHistory.count > maxHistoryEntries {
|
|
304
|
+
syncHistory.removeFirst(syncHistory.count - maxHistoryEntries)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Calculates average sync duration
|
|
309
|
+
private func calculateAverageDuration() -> TimeInterval {
|
|
310
|
+
let successfulSyncs = syncHistory.filter { $0.success && $0.duration > 0 }
|
|
311
|
+
guard !successfulSyncs.isEmpty else { return 0 }
|
|
312
|
+
|
|
313
|
+
let totalDuration = successfulSyncs.reduce(0.0) { $0 + $1.duration }
|
|
314
|
+
return totalDuration / Double(successfulSyncs.count)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Notifies all observers
|
|
318
|
+
private func notifyObservers(_ action: (SyncObserver) -> Void) {
|
|
319
|
+
// Clean up deallocated observers
|
|
320
|
+
observers.removeAll { $0.observer == nil }
|
|
321
|
+
|
|
322
|
+
// Notify active observers
|
|
323
|
+
for weakObserver in observers {
|
|
324
|
+
if let observer = weakObserver.observer {
|
|
325
|
+
action(observer)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// MARK: - Supporting Types
|
|
332
|
+
|
|
333
|
+
/// Configuration for background sync behavior
|
|
334
|
+
public struct SyncConfiguration: Sendable {
|
|
335
|
+
public let activeSyncInterval: TimeInterval
|
|
336
|
+
public let backgroundSyncInterval: TimeInterval
|
|
337
|
+
public let syncOnForeground: Bool
|
|
338
|
+
public let syncOnBackground: Bool
|
|
339
|
+
public let defaultSyncOptions: SyncOptions
|
|
340
|
+
|
|
341
|
+
public init(
|
|
342
|
+
activeSyncInterval: TimeInterval = 300,
|
|
343
|
+
backgroundSyncInterval: TimeInterval = 3600,
|
|
344
|
+
syncOnForeground: Bool = true,
|
|
345
|
+
syncOnBackground: Bool = true,
|
|
346
|
+
defaultSyncOptions: SyncOptions = SyncOptions()
|
|
347
|
+
) {
|
|
348
|
+
self.activeSyncInterval = activeSyncInterval
|
|
349
|
+
self.backgroundSyncInterval = backgroundSyncInterval
|
|
350
|
+
self.syncOnForeground = syncOnForeground
|
|
351
|
+
self.syncOnBackground = syncOnBackground
|
|
352
|
+
self.defaultSyncOptions = defaultSyncOptions
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public static let `default` = SyncConfiguration()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Statistics about sync operations
|
|
359
|
+
public struct SyncStatistics: Sendable {
|
|
360
|
+
public let lastSyncDate: Date?
|
|
361
|
+
public let totalSyncs: Int
|
|
362
|
+
public let successfulSyncs: Int
|
|
363
|
+
public let failedSyncs: Int
|
|
364
|
+
public let averageDuration: TimeInterval
|
|
365
|
+
public let syncHistory: [SyncHistoryEntry]
|
|
366
|
+
|
|
367
|
+
public var successRate: Double {
|
|
368
|
+
guard totalSyncs > 0 else { return 0 }
|
|
369
|
+
return Double(successfulSyncs) / Double(totalSyncs) * 100
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public struct SyncHistoryEntry: Sendable, Codable {
|
|
374
|
+
public let timestamp: Date
|
|
375
|
+
public let success: Bool
|
|
376
|
+
public let duration: TimeInterval
|
|
377
|
+
public let pushedCount: Int
|
|
378
|
+
public let fetchedCount: Int
|
|
379
|
+
public let error: String?
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private struct WeakSyncObserver {
|
|
383
|
+
weak var observer: SyncObserver?
|
|
384
|
+
init(_ observer: SyncObserver) { self.observer = observer }
|
|
385
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Sync Conflict Resolver
|
|
4
|
+
|
|
5
|
+
/// Default implementation of conflict resolution for synchronized records
|
|
6
|
+
///
|
|
7
|
+
/// This resolver implements multiple conflict resolution strategies:
|
|
8
|
+
/// - Last-write-wins (default): Most recently modified record wins
|
|
9
|
+
/// - Prefer local: Always keep local version
|
|
10
|
+
/// - Prefer remote: Always keep remote version
|
|
11
|
+
/// - Highest version: Keep record with highest version number
|
|
12
|
+
/// - Merge: Attempt to merge both versions (custom logic required)
|
|
13
|
+
public actor SyncConflictResolver: SyncConflictResolverProtocol {
|
|
14
|
+
|
|
15
|
+
// MARK: - Properties
|
|
16
|
+
|
|
17
|
+
/// Statistics about resolved conflicts
|
|
18
|
+
public private(set) var conflictStats: ConflictStatistics
|
|
19
|
+
|
|
20
|
+
/// Custom merge handlers for specific record types
|
|
21
|
+
private var mergeHandlers: [String: MergeHandler] = [:]
|
|
22
|
+
|
|
23
|
+
// MARK: - Initialization
|
|
24
|
+
|
|
25
|
+
public init() {
|
|
26
|
+
self.conflictStats = ConflictStatistics()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Public Methods
|
|
30
|
+
|
|
31
|
+
/// Resolves a conflict between local and remote versions
|
|
32
|
+
///
|
|
33
|
+
/// - Parameters:
|
|
34
|
+
/// - local: The local version of the record
|
|
35
|
+
/// - remote: The remote version of the record
|
|
36
|
+
/// - strategy: The resolution strategy to apply
|
|
37
|
+
/// - Returns: The resolved record that should be kept
|
|
38
|
+
public func resolve(
|
|
39
|
+
local: SyncableRecord,
|
|
40
|
+
remote: SyncableRecord,
|
|
41
|
+
strategy: ConflictResolutionStrategy
|
|
42
|
+
) async -> SyncableRecord {
|
|
43
|
+
// Validate that records are the same type
|
|
44
|
+
guard local.recordID == remote.recordID else {
|
|
45
|
+
return local // Fallback to local if IDs don't match
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let resolved: SyncableRecord
|
|
49
|
+
|
|
50
|
+
switch strategy {
|
|
51
|
+
case .lastWriteWins:
|
|
52
|
+
resolved = resolveLastWriteWins(local: local, remote: remote)
|
|
53
|
+
|
|
54
|
+
case .preferLocal:
|
|
55
|
+
resolved = local
|
|
56
|
+
|
|
57
|
+
case .preferRemote:
|
|
58
|
+
resolved = remote
|
|
59
|
+
|
|
60
|
+
case .highestVersion:
|
|
61
|
+
resolved = resolveHighestVersion(local: local, remote: remote)
|
|
62
|
+
|
|
63
|
+
case .merge:
|
|
64
|
+
resolved = resolveMerge(local: local, remote: remote)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update statistics
|
|
68
|
+
conflictStats.recordConflict(
|
|
69
|
+
recordType: local.recordType,
|
|
70
|
+
strategy: strategy,
|
|
71
|
+
winner: resolved.recordID == local.recordID ? .local : .remote
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return resolved
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Registers a custom merge handler for a specific record type
|
|
78
|
+
///
|
|
79
|
+
/// - Parameters:
|
|
80
|
+
/// - recordType: The CloudKit record type name
|
|
81
|
+
/// - handler: Closure that merges two records of this type
|
|
82
|
+
public func registerMergeHandler(
|
|
83
|
+
for recordType: String,
|
|
84
|
+
handler: @escaping MergeHandler
|
|
85
|
+
) {
|
|
86
|
+
mergeHandlers[recordType] = handler
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Resets conflict statistics
|
|
90
|
+
public func resetStatistics() {
|
|
91
|
+
conflictStats = ConflictStatistics()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: - Private Resolution Methods
|
|
95
|
+
|
|
96
|
+
/// Resolves conflict by keeping the most recently modified record
|
|
97
|
+
private func resolveLastWriteWins(
|
|
98
|
+
local: SyncableRecord,
|
|
99
|
+
remote: SyncableRecord
|
|
100
|
+
) -> SyncableRecord {
|
|
101
|
+
if local.modifiedAt > remote.modifiedAt {
|
|
102
|
+
return local
|
|
103
|
+
} else if remote.modifiedAt > local.modifiedAt {
|
|
104
|
+
return remote
|
|
105
|
+
} else {
|
|
106
|
+
// Same timestamp, use version as tiebreaker
|
|
107
|
+
return local.version >= remote.version ? local : remote
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Resolves conflict by keeping the record with highest version number
|
|
112
|
+
private func resolveHighestVersion(
|
|
113
|
+
local: SyncableRecord,
|
|
114
|
+
remote: SyncableRecord
|
|
115
|
+
) -> SyncableRecord {
|
|
116
|
+
if local.version > remote.version {
|
|
117
|
+
return local
|
|
118
|
+
} else if remote.version > local.version {
|
|
119
|
+
return remote
|
|
120
|
+
} else {
|
|
121
|
+
// Same version, use timestamp as tiebreaker
|
|
122
|
+
return local.modifiedAt >= remote.modifiedAt ? local : remote
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Attempts to merge both versions using custom merge logic
|
|
127
|
+
private func resolveMerge(
|
|
128
|
+
local: SyncableRecord,
|
|
129
|
+
remote: SyncableRecord
|
|
130
|
+
) -> SyncableRecord {
|
|
131
|
+
// Check for registered merge handler
|
|
132
|
+
if let handler = mergeHandlers[local.recordType] {
|
|
133
|
+
return handler(local, remote)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// No custom merge handler, fall back to last-write-wins
|
|
137
|
+
return resolveLastWriteWins(local: local, remote: remote)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: - Supporting Types
|
|
142
|
+
|
|
143
|
+
/// Closure type for custom merge handlers
|
|
144
|
+
public typealias MergeHandler = @Sendable (SyncableRecord, SyncableRecord) -> SyncableRecord
|
|
145
|
+
|
|
146
|
+
/// Indicates which version won during conflict resolution
|
|
147
|
+
public enum ConflictWinner: String, Codable, Sendable {
|
|
148
|
+
case local
|
|
149
|
+
case remote
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Statistics about conflict resolution operations
|
|
153
|
+
public struct ConflictStatistics: Sendable {
|
|
154
|
+
/// Total number of conflicts resolved
|
|
155
|
+
public private(set) var totalConflicts: Int = 0
|
|
156
|
+
|
|
157
|
+
/// Number of conflicts per record type
|
|
158
|
+
public private(set) var conflictsByType: [String: Int] = [:]
|
|
159
|
+
|
|
160
|
+
/// Number of conflicts per strategy
|
|
161
|
+
public private(set) var conflictsByStrategy: [String: Int] = [:]
|
|
162
|
+
|
|
163
|
+
/// Number of wins for local vs remote
|
|
164
|
+
public private(set) var localWins: Int = 0
|
|
165
|
+
public private(set) var remoteWins: Int = 0
|
|
166
|
+
|
|
167
|
+
/// Timestamp of last conflict
|
|
168
|
+
public private(set) var lastConflictAt: Date?
|
|
169
|
+
|
|
170
|
+
public init() {}
|
|
171
|
+
|
|
172
|
+
/// Records a conflict resolution event
|
|
173
|
+
mutating func recordConflict(
|
|
174
|
+
recordType: String,
|
|
175
|
+
strategy: ConflictResolutionStrategy,
|
|
176
|
+
winner: ConflictWinner
|
|
177
|
+
) {
|
|
178
|
+
totalConflicts += 1
|
|
179
|
+
conflictsByType[recordType, default: 0] += 1
|
|
180
|
+
conflictsByStrategy[strategy.rawValue, default: 0] += 1
|
|
181
|
+
|
|
182
|
+
switch winner {
|
|
183
|
+
case .local:
|
|
184
|
+
localWins += 1
|
|
185
|
+
case .remote:
|
|
186
|
+
remoteWins += 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
lastConflictAt = Date()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Percentage of conflicts won by local version
|
|
193
|
+
public var localWinRate: Double {
|
|
194
|
+
guard totalConflicts > 0 else { return 0 }
|
|
195
|
+
return Double(localWins) / Double(totalConflicts)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Percentage of conflicts won by remote version
|
|
199
|
+
public var remoteWinRate: Double {
|
|
200
|
+
guard totalConflicts > 0 else { return 0 }
|
|
201
|
+
return Double(remoteWins) / Double(totalConflicts)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Human-readable summary of conflict statistics
|
|
205
|
+
public var summary: String {
|
|
206
|
+
var lines = [
|
|
207
|
+
"Conflict Resolution Statistics:",
|
|
208
|
+
" Total conflicts: \(totalConflicts)",
|
|
209
|
+
" Local wins: \(localWins) (\(String(format: "%.1f%%", localWinRate * 100)))",
|
|
210
|
+
" Remote wins: \(remoteWins) (\(String(format: "%.1f%%", remoteWinRate * 100)))"
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if !conflictsByType.isEmpty {
|
|
214
|
+
lines.append(" By record type:")
|
|
215
|
+
for (type, count) in conflictsByType.sorted(by: { $0.value > $1.value }) {
|
|
216
|
+
lines.append(" \(type): \(count)")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if !conflictsByStrategy.isEmpty {
|
|
221
|
+
lines.append(" By strategy:")
|
|
222
|
+
for (strategy, count) in conflictsByStrategy.sorted(by: { $0.value > $1.value }) {
|
|
223
|
+
lines.append(" \(strategy): \(count)")
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if let lastConflict = lastConflictAt {
|
|
228
|
+
let formatter = RelativeDateTimeFormatter()
|
|
229
|
+
let timeAgo = formatter.localizedString(for: lastConflict, relativeTo: Date())
|
|
230
|
+
lines.append(" Last conflict: \(timeAgo)")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return lines.joined(separator: "\n")
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// MARK: - Default Merge Handlers
|
|
238
|
+
|
|
239
|
+
extension SyncConflictResolver {
|
|
240
|
+
/// Registers default merge handlers for Runic record types
|
|
241
|
+
public func registerDefaultMergeHandlers() {
|
|
242
|
+
// Usage Snapshot merge: prefer record with most recent data
|
|
243
|
+
registerMergeHandler(for: CloudKitRecordType.usageSnapshot) { local, remote in
|
|
244
|
+
guard let localUsage = local as? UsageSnapshotSyncRecord,
|
|
245
|
+
let remoteUsage = remote as? UsageSnapshotSyncRecord else {
|
|
246
|
+
return local
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prefer the record with more recent usage data
|
|
250
|
+
return localUsage.updatedAt > remoteUsage.updatedAt ? local : remote
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// User Preferences merge: combine enabled providers
|
|
254
|
+
registerMergeHandler(for: CloudKitRecordType.userPreferences) { local, remote in
|
|
255
|
+
guard let localPrefs = local as? UserPreferencesSyncRecord,
|
|
256
|
+
let remotePrefs = remote as? UserPreferencesSyncRecord else {
|
|
257
|
+
return local
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Merge enabled providers (union of both sets)
|
|
261
|
+
let mergedProviders = Array(Set(localPrefs.enabledProviders + remotePrefs.enabledProviders))
|
|
262
|
+
|
|
263
|
+
// Use most recent settings for other fields
|
|
264
|
+
let source = localPrefs.modifiedAt > remotePrefs.modifiedAt ? localPrefs : remotePrefs
|
|
265
|
+
|
|
266
|
+
return UserPreferencesSyncRecord(
|
|
267
|
+
recordID: localPrefs.recordID,
|
|
268
|
+
version: max(localPrefs.version, remotePrefs.version) + 1,
|
|
269
|
+
modifiedAt: Date(),
|
|
270
|
+
lastModifiedDeviceID: source.lastModifiedDeviceID,
|
|
271
|
+
refreshInterval: source.refreshInterval,
|
|
272
|
+
enabledProviders: mergedProviders,
|
|
273
|
+
notificationsEnabled: source.notificationsEnabled,
|
|
274
|
+
autoRefreshEnabled: source.autoRefreshEnabled,
|
|
275
|
+
theme: source.theme,
|
|
276
|
+
displayFormat: source.displayFormat
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Alert Configuration merge: prefer most restrictive thresholds
|
|
281
|
+
registerMergeHandler(for: CloudKitRecordType.alertConfiguration) { local, remote in
|
|
282
|
+
guard let localAlert = local as? AlertConfigurationSyncRecord,
|
|
283
|
+
let remoteAlert = remote as? AlertConfigurationSyncRecord else {
|
|
284
|
+
return local
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Use lower (more conservative) thresholds
|
|
288
|
+
let warningThreshold = min(localAlert.warningThreshold, remoteAlert.warningThreshold)
|
|
289
|
+
let criticalThreshold = min(localAlert.criticalThreshold, remoteAlert.criticalThreshold)
|
|
290
|
+
|
|
291
|
+
// Merge notification channels (union)
|
|
292
|
+
let mergedChannels = Array(Set(localAlert.notificationChannels + remoteAlert.notificationChannels))
|
|
293
|
+
|
|
294
|
+
// Enabled if either version has it enabled
|
|
295
|
+
let enabled = localAlert.enabled || remoteAlert.enabled
|
|
296
|
+
|
|
297
|
+
return AlertConfigurationSyncRecord(
|
|
298
|
+
recordID: localAlert.recordID,
|
|
299
|
+
version: max(localAlert.version, remoteAlert.version) + 1,
|
|
300
|
+
modifiedAt: Date(),
|
|
301
|
+
lastModifiedDeviceID: localAlert.lastModifiedDeviceID,
|
|
302
|
+
providerID: localAlert.providerID,
|
|
303
|
+
warningThreshold: warningThreshold,
|
|
304
|
+
criticalThreshold: criticalThreshold,
|
|
305
|
+
notificationChannels: mergedChannels,
|
|
306
|
+
enabled: enabled
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|