diff --git a/.github/actions/setup-sundialkit/action.yml b/.github/actions/setup-sundialkit/action.yml new file mode 100644 index 00000000..e8ac1b44 --- /dev/null +++ b/.github/actions/setup-sundialkit/action.yml @@ -0,0 +1,50 @@ +name: Setup SundialKit +description: Replaces the local SundialKit path dependency with a remote reference, pinned to the branch's current commit + +inputs: + branch: + description: SundialKit branch to use (leave empty to keep the local path dependency) + +runs: + using: composite + steps: + # Resolve the branch to its current HEAD commit and pin the dependency by + # `revision:` rather than `branch:`. This makes the dependency content-addressed, + # so `swift package dump-package` (which swift-build@v1 hashes for its cache key) + # changes whenever the SundialKit branch advances — otherwise a new SundialKit commit + # on the same branch yields a stale cache hit and is never rebuilt. Falls back to a + # `branch:` pin if the ref can't be resolved (e.g. offline). + - name: Update Package.swift (Unix) + if: inputs.branch != '' && runner.os != 'Windows' + shell: bash + run: | + BRANCH='${{ inputs.branch }}' + REF=$(git ls-remote https://github.com/brightdigit/SundialKit.git "$BRANCH" | head -n1 | cut -f1) + if [ -n "$REF" ]; then + REQ='revision: "'"$REF"'"' + echo "Pinning SundialKit to $BRANCH @ $REF" + else + REQ='branch: "'"$BRANCH"'"' + echo "Could not resolve $BRANCH to a commit; pinning by branch" + fi + if [ "$RUNNER_OS" = "macOS" ]; then + sed -i '' 's|\.package(name: "SundialKit", path: "\.\./SundialKit")|.package(url: "https://github.com/brightdigit/SundialKit.git", '"$REQ"')|g' Package.swift + else + sed -i 's|\.package(name: "SundialKit", path: "\.\./SundialKit")|.package(url: "https://github.com/brightdigit/SundialKit.git", '"$REQ"')|g' Package.swift + fi + rm -f Package.resolved + - name: Update Package.swift (Windows) + if: inputs.branch != '' && runner.os == 'Windows' + shell: pwsh + run: | + $branch = '${{ inputs.branch }}' + $ref = (git ls-remote https://github.com/brightdigit/SundialKit.git $branch | Select-Object -First 1) -split "`t" | Select-Object -First 1 + if ($ref) { + $req = "revision: `"$ref`"" + Write-Host "Pinning SundialKit to $branch @ $ref" + } else { + $req = "branch: `"$branch`"" + Write-Host "Could not resolve $branch to a commit; pinning by branch" + } + (Get-Content Package.swift) -replace '\.package\(name: "SundialKit", path: "\.\./SundialKit"\)', ".package(url: `"https://github.com/brightdigit/SundialKit.git`", $req)" | Set-Content Package.swift + Remove-Item -Path Package.resolved -Force -ErrorAction SilentlyContinue diff --git a/.github/workflows/SundialKit.yml b/.github/workflows/SundialKit.yml index 8b959b26..c0dff916 100644 --- a/.github/workflows/SundialKit.yml +++ b/.github/workflows/SundialKit.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - atleast-beta.6 tags: - 'v[0-9]*.[0-9]*.[0-9]*' paths-ignore: @@ -14,6 +15,7 @@ on: branches: - main - 'v[0-9]*.[0-9]*.[0-9]*' + - atleast-beta.6 paths-ignore: - '**.md' - 'Docs/**' @@ -39,7 +41,7 @@ jobs: - name: Determine build matrix id: set-matrix run: | - if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.event_name }}" == "pull_request" ]]; then + if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/atleast-beta.6" || "${{ github.event_name }}" == "pull_request" ]]; then echo "full-matrix=true" >> "$GITHUB_OUTPUT" echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT" echo 'ubuntu-swift=[{"version":"6.1"},{"version":"6.2"},{"version":"6.3"}]' >> "$GITHUB_OUTPUT" @@ -135,7 +137,61 @@ jobs: include: # SPM Build — no platform type; matrix.type evaluates to '' by design - runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" + - runs-on: macos-26 + xcode: "/Applications/Xcode_26.1.app" + - runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + + # iOS Build Matrix - Xcode 16.x+ (Swift 6.x) + - type: ios + runs-on: macos-26 + xcode: "/Applications/Xcode_26.5.app" + deviceName: "iPhone 17 Pro" + osVersion: "26.5" + download-platform: true + + - type: ios + runs-on: macos-26 + xcode: "/Applications/Xcode_26.1.app" + deviceName: "iPhone 17 Pro" + osVersion: "26.1" + download-platform: true + + - type: ios + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "iPhone 16e" + osVersion: "18.5" + + # watchOS Build Matrix - Xcode 16.x+ (Swift 6.x) + - type: watchos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.5.app" + deviceName: "Apple Watch Ultra 3 (49mm)" + osVersion: "26.5" + download-platform: true + + - type: watchos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.1.app" + deviceName: "Apple Watch Series 11 (46mm)" + osVersion: "26.1" + download-platform: true + + - type: watchos + runs-on: macos-15 + xcode: "/Applications/Xcode_16.4.app" + deviceName: "Apple Watch Series 10 (46mm)" + osVersion: "11.5" + + # visionOS Build Matrix - Xcode 16.x+ (Swift 6.x) + - type: visionos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.5.app" + deviceName: "Apple Vision Pro" + osVersion: "26.5" + download-platform: true steps: - uses: actions/checkout@v6 @@ -183,38 +239,38 @@ jobs: # macOS Build - type: macos runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" # iOS Build Matrix - type: ios runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "iPhone 17 Pro" - osVersion: "26.4" + osVersion: "26.5" download-platform: true # watchOS Build Matrix - type: watchos runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "Apple Watch Ultra 3 (49mm)" - osVersion: "26.4" + osVersion: "26.5" download-platform: true # tvOS Build Matrix - type: tvos runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "Apple TV" - osVersion: "26.4" + osVersion: "26.5" download-platform: true # visionOS Build Matrix - type: visionos runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "Apple Vision Pro" - osVersion: "26.4" + osVersion: "26.5" download-platform: true steps: @@ -253,7 +309,10 @@ jobs: swift: - version: "6.2" - version: "6.3" - android-api-level: [33, 34, 35, 36] + # API 36 dropped: Swift 6.x Android SDK lacks API-36 CRT objects + # (crtbegin_dynamic.o/crtend_android.o), link fails. Re-add when the + # SDK ships an NDK r28+ sysroot. See skiptools/swift-android-action. + android-api-level: [33, 34, 35] steps: - uses: actions/checkout@v6 - name: Free disk space diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 27c8168d..2dd7fe40 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,7 +17,7 @@ on: - '*WIP' pull_request: # The branches below must be a subset of the branches above - branches: [ "main" , "v2.0.0" ] + branches: [ "main" ] schedule: - cron: '20 11 * * 3' @@ -29,7 +29,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-15') || 'ubuntu-latest' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-26') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -47,10 +47,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Xcode - run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.5.app/Contents/Developer - name: Verify Swift Version run: | @@ -59,7 +59,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -77,6 +77,6 @@ jobs: swift build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/sundial-demo.yml b/.github/workflows/sundial-demo.yml index 2d18de71..8c17b45d 100644 --- a/.github/workflows/sundial-demo.yml +++ b/.github/workflows/sundial-demo.yml @@ -31,20 +31,20 @@ jobs: fail-fast: false matrix: include: - # iOS Build - Xcode 26.4 on macOS-26 + # iOS Build - Xcode 26.5 on macOS-26 - type: ios runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "iPhone 17 Pro" - osVersion: "26.4" + osVersion: "26.5" download-platform: true - # watchOS Build - Xcode 26.4 on macOS-26 + # watchOS Build - Xcode 26.5 on macOS-26 - type: watchos runs-on: macos-26 - xcode: "/Applications/Xcode_26.4.app" + xcode: "/Applications/Xcode_26.5.app" deviceName: "Apple Watch Ultra 3 (49mm)" - osVersion: "26.4" + osVersion: "26.5" download-platform: true steps: @@ -111,7 +111,13 @@ jobs: working-directory: ${{ env.DEMO_PATH }} - name: Select Xcode - run: sudo xcode-select -s "/Applications/Xcode_26.4.app" + run: sudo xcode-select -s "/Applications/Xcode_26.5.app" + + - name: Download iOS platform + run: xcodebuild -downloadPlatform iOS + + - name: Download watchOS platform + run: xcodebuild -downloadPlatform watchOS - name: Setup SSH for AppCerts repository uses: webfactory/ssh-agent@v0.9.0 @@ -144,7 +150,7 @@ jobs: FASTLANE_KEYCHAIN_PASSWORD: ${{ steps.keychain.outputs.password }} APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_BASE64 }} DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} - name: Upload Pulse IPA @@ -199,7 +205,7 @@ jobs: env: APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_BASE64 }} - name: Increment build number and commit if: success() diff --git a/.mise.toml b/.mise.toml index bb8a74f5..c4c6e9c8 100644 --- a/.mise.toml +++ b/.mise.toml @@ -5,8 +5,8 @@ ruby = "3.3.0" # Swift development tools (migrated from Mint) # Using Swift Package Manager plugins "spm:swiftlang/swift-format" = "602.0.0" -swiftlint = "0.61.0" -"spm:peripheryapp/periphery" = "3.2.0" +"aqua:realm/SwiftLint" = "0.62.2" +"spm:peripheryapp/periphery" = "3.7.4" # Xcode project generation xcodegen = "2.43.0" diff --git a/Examples/Sundial/.mise.toml b/Examples/Sundial/.mise.toml new file mode 100644 index 00000000..d062f7bb --- /dev/null +++ b/Examples/Sundial/.mise.toml @@ -0,0 +1,6 @@ +[tools] +# Protocol Buffer Swift plugin (migrated from Mint) +"spm:apple/swift-protobuf" = "1.38.0" + +[settings] +experimental = true diff --git a/Examples/Sundial/Documentation/IMPLEMENTATION_PLAN.md b/Examples/Sundial/Documentation/IMPLEMENTATION_PLAN.md deleted file mode 100644 index b2158665..00000000 --- a/Examples/Sundial/Documentation/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,413 +0,0 @@ -# Sundial Demo - Implementation Plan - -**Status**: Phase 1 Complete - Foundation Established -**Last Updated**: 2025-10-26 -**Task Master**: Task 13 - Migrate Sundial demo application to SundialKit v2.0.0 - -## Project Overview - -This demo showcases SundialKit v2.0.0 with focus on: -1. **Binary Protobuf Messaging** - Efficient binary encoding with BinaryMessagable -2. **Latency Measurement** - Round-trip time tracking across transport methods -3. **Transport Route Comparison** - sendMessage vs updateApplicationContext behavior - -Two implementation variants: -- **SundialDemoCombine**: @MainActor + Combine publishers (v1 compatibility) -- **SundialDemoStream**: Actor-based + AsyncStream (modern async/await) - ---- - -## ✅ Phase 1: Foundation (COMPLETED) - -### Directory Structure -``` -Examples/Sundial/ -├── Protos/ # Protocol Buffer schemas ✓ -│ ├── color_message.proto # Simple color (16 bytes) ✓ -│ ├── complex_message.proto # Complex nested (256+ bytes) ✓ -│ └── latency_test.proto # Ping/pong testing ✓ -├── Sources/ -│ ├── Shared/ # Common code ✓ -│ │ ├── Models/ -│ │ │ ├── ProtoExtensions.swift # BinaryMessagable conformance ✓ -│ │ │ ├── LatencyTracker.swift # Latency tracking ✓ -│ │ │ └── TransportMethod.swift # Transport enum ✓ -│ │ ├── Utilities/ -│ │ │ ├── Color+Components.swift # Color helpers ✓ -│ │ │ └── Date+Milliseconds.swift # Date helpers ✓ -│ │ └── Views/ # (Next phase) -│ ├── SundialDemoCombine/ # (Next phase) -│ └── SundialDemoStream/ # (Next phase) -├── Scripts/ -│ └── generate-protos.sh # Protobuf generation script ✓ -├── Package.swift # SPM configuration ✓ -└── README.md # Documentation ✓ -``` - -### Key Accomplishments - -1. **Protobuf Schemas Defined** ✓ - - `ColorMessage`: Simple color data (red/green/blue/alpha + timestamp) - - `ComplexMessage`: Nested message with sensors, device info, color history - - `LatencyTestRequest/Reply`: Ping/pong with timestamps - -2. **BinaryMessagable Direct Conformance** ✓ - - No wrapper pattern needed - - Simple extensions on SwiftProtobuf types - - Convenience helpers for SwiftUI Color conversion - -3. **Shared Infrastructure** ✓ - - `TransportMethod` enum with metadata - - `LatencyTracker` for performance measurement - - Utility extensions for Date and Color - -4. **Build Configuration** ✓ - - Package.swift with all dependencies - - Script for protobuf code generation - - Comprehensive README - ---- - -## 📋 Phase 2: Shared UI Components (NEXT) - -### Components to Create - -#### 1. MetricCard.swift -Reusable card for displaying key-value metrics: -```swift -struct MetricCard: View { - let title: String - let value: String - let subtitle: String? - let icon: String - let color: Color -} -``` - -Used for: -- RTT display -- Payload size -- Success rate -- Encode/decode times - -#### 2. TransportBadge.swift -Visual indicator for transport method: -```swift -struct TransportBadge: View { - let method: TransportMethod - let isActive: Bool -} -``` - -Shows icon + name with color coding. - -#### 3. LatencyGraph.swift -Line chart for latency history: -```swift -struct LatencyGraph: View { - let measurements: [LatencyTracker.Measurement] - let height: CGFloat = 150 -} -``` - -Displays last 20 measurements with color coding by transport method. - -#### 4. MessageHistoryRow.swift -Row item for message history list: -```swift -struct MessageHistoryRow: View { - let timestamp: Date - let method: TransportMethod - let size: Int - let rtt: TimeInterval? - let success: Bool -} -``` - -#### 5. ConnectionStatusView.swift -Real-time connection health indicator: -```swift -struct ConnectionStatusView: View { - let isReachable: Bool - let activationState: ActivationState - let lastUpdate: Date -} -``` - -Compact footer showing network + connectivity status. - -#### 6. ColorPreview.swift -Color circle with metadata: -```swift -struct ColorPreview: View { - let color: Color - let timestamp: Date? - let source: String? - let size: CGFloat = 60 -} -``` - ---- - -## 📋 Phase 3: Combine Variant Implementation - -### 3.1 App Structure - -**SundialApp.swift** -```swift -@main -struct SundialApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} -``` - -**ContentView.swift** -```swift -struct ContentView: View { - var body: some View { - TabView { - MessageLabView() - .tabItem { Label("Transport", systemImage: "arrow.left.arrow.right") } - - LatencyDashboardView() - .tabItem { Label("Latency", systemImage: "clock") } - - ProtocolComparisonView() - .tabItem { Label("Protocol", systemImage: "chart.bar") } - - DiagnosticsView() - .tabItem { Label("Diagnostics", systemImage: "wrench") } - } - } -} -``` - -### 3.2 ViewModels - -Each tab needs a ViewModel with ConnectivityObserver and/or NetworkObserver: - -**MessageLabViewModel.swift** -- Manages message payload builder -- Handles transport method selection -- Sends/receives ColorMessage and ComplexMessage -- Tracks send status - -**LatencyViewModel.swift** -- Uses LatencyTracker -- Sends LatencyTestRequest messages -- Processes LatencyTestReply -- Generates statistics - -**ProtocolViewModel.swift** -- Compares Dictionary vs Protobuf encoding -- Measures encoding/decoding times -- Calculates size savings -- Runs interactive tests - -**DiagnosticsViewModel.swift** -- Monitors connection health -- Displays error log -- Shows performance counters -- Provides raw state dump - -### 3.3 Views - -#### Tab 1: MessageLabView.swift -Sections: -1. Payload Builder (complexity slider) -2. Transport Method Control (auto/manual override) -3. Send Button -4. Sent/Received Display - -#### Tab 2: LatencyDashboardView.swift -Sections: -1. Live RTT Metrics (large display) -2. Statistics Cards (avg/min/max/stddev) -3. Latency Breakdown (encode/network/decode) -4. History Graph -5. Test Runner - -#### Tab 3: ProtocolComparisonView.swift -Sections: -1. Encoding Comparison (side-by-side) -2. Complexity Scaling Table -3. Interactive Test -4. Protocol Schema Display - -#### Tab 4: DiagnosticsView.swift -Sections: -1. Connection Health (reachability + session state) -2. Message Queue Status -3. Transport History (ScrollView) -4. Error Log -5. Performance Counters - ---- - -## 📋 Phase 4: Stream Variant Implementation - -Port all Combine ViewModels to Stream equivalents: -- Replace @Published with AsyncStream -- Replace Combine publishers with actor methods -- Use async/await throughout -- Maintain identical UI - ---- - -## 📋 Phase 5: Testing & Polish - -### Integration Tests -- Protobuf encoding/decoding -- Transport method selection logic -- Latency measurement accuracy -- UI state updates - -### UI Tests -- Tab navigation -- Message sending flow -- Error handling -- Connection state transitions - -### Polish -- Animations -- Error messages -- Loading states -- Accessibility - ---- - -## Implementation Notes - -### Protobuf Code Generation - -Before building, generate Swift code from .proto files: - -```bash -cd Examples/Sundial -./Scripts/generate-protos.sh -``` - -This creates `Sources/Shared/Generated/*.pb.swift` files. - -### Direct BinaryMessagable Conformance - -Key insight: No wrapper pattern needed! - -```swift -extension Sundial_Demo_ColorMessage: BinaryMessagable { - public init(from data: Data) throws { - try self.init(serializedData: data) // SwiftProtobuf already has this! - } - - public func encode() throws -> Data { - try serializedData() // SwiftProtobuf already has this! - } -} -``` - -### Transport Method Selection Logic - -```swift -func selectTransportMethod(isReachable: Bool, override: TransportMethod?) -> TransportMethod { - if let override = override { - // Manual override for testing - guard !override.requiresReachability || isReachable else { - throw TransportError.notReachable - } - return override - } - - // Automatic selection - return isReachable ? .sendMessage : .updateApplicationContext -} -``` - -### Latency Measurement Pattern - -```swift -// 1. Start timer -let sendTime = Date() - -// 2. Encode -let encodeStart = Date() -let data = try message.encode() -let encodeTime = Date().timeIntervalSince(encodeStart) - -// 3. Send and await reply -let reply = try await connectivityObserver.sendMessage(data) - -// 4. Decode -let decodeStart = Date() -let replyMsg = try LatencyTestReply(from: reply) -let decodeTime = Date().timeIntervalSince(decodeStart) - -// 5. Record measurement -let receiveTime = Date() -latencyTracker.recordMeasurement( - .init( - sequenceNumber: sequence, - sendTime: sendTime, - receiveTime: receiveTime, - transportMethod: .sendMessage, - payloadSize: data.count, - encodeTime: encodeTime, - decodeTime: decodeTime - ) -) -``` - ---- - -## Success Criteria - -- ✅ Protobuf messages show 75%+ size reduction vs dictionary -- ✅ Latency measurements accurate within 5ms -- ✅ Transport method selection correct based on reachability -- ✅ All routes work (sendMessage, updateApplicationContext, sendMessageData) -- ✅ Complex messages encode/decode correctly -- ✅ Connection transitions handled gracefully -- ✅ UI clearly explains behavior -- ✅ Both variants behave identically - ---- - -## Next Session Tasks - -1. **Generate Protobuf Code** - ```bash - cd Examples/Sundial - mkdir -p Sources/Shared/Generated - ./Scripts/generate-protos.sh - ``` - -2. **Create Shared UI Components** - - Start with MetricCard (most reusable) - - Then TransportBadge - - Then ColorPreview - - Test with SwiftUI previews - -3. **Implement First Tab (Combine Variant)** - - MessageLabViewModel - - MessageLabView - - Wire up ConnectivityObserver - - Test message sending - -4. **Continue with remaining tabs** - ---- - -## References - -- **Task Master**: Task 13, Subtask 13.2 (v1.0.0 validation baseline) -- **CLAUDE.md**: Main project documentation -- **Package.swift**: Dependencies and target configuration -- **Protos/**: Protocol Buffer schema definitions -- **Sources/Shared/Models/ProtoExtensions.swift**: BinaryMessagable conformance examples - ---- - -**Resume Point**: Phase 2 - Create shared UI components starting with MetricCard.swift diff --git a/Examples/Sundial/Documentation/RESUME_HERE.md b/Examples/Sundial/Documentation/RESUME_HERE.md deleted file mode 100644 index 504db8cc..00000000 --- a/Examples/Sundial/Documentation/RESUME_HERE.md +++ /dev/null @@ -1,301 +0,0 @@ -# 🚀 Resume Development Here - -**Last Session**: 2025-10-27 -**Current Phase**: Phase 2 Complete ✅ → Phase 3 Next 📋 -**Task Master**: Task 13.2 (Establish v1.0.0 API validation baseline) - ---- - -## ✅ What's Been Completed - -### Foundation (Phase 1) -- ✅ Project directory structure -- ✅ Protocol Buffer schemas (3 files in `Protos/`) -- ✅ BinaryMessagable conformance (direct, no wrappers!) -- ✅ Shared models (TransportMethod, LatencyTracker) -- ✅ Utility extensions (Color, Date) -- ✅ Package.swift configuration -- ✅ Build scripts and documentation - -### Shared UI Components (Phase 2) ✅ NEW! -- ✅ Mint setup for swift-protobuf tools (Mintfile) -- ✅ Generated Swift code from .proto schemas (3 files) -- ✅ MetricCard.swift - Reusable metric display -- ✅ ColorPreview.swift - Color circle with metadata -- ✅ TransportBadge.swift - Transport method badges -- ✅ ConnectionStatusView.swift - Connection health footer -- ✅ LatencyGraph.swift - Line chart with SwiftUI Charts -- ✅ MessageHistoryRow.swift - Message log list rows - -### Files Created (19 total, +8 in Phase 2) -``` -Protos/ - ├── color_message.proto - ├── complex_message.proto - └── latency_test.proto - -Sources/Shared/ - ├── Generated/ ✅ NEW! - │ ├── color_message.pb.swift (4.2KB) - │ ├── complex_message.pb.swift (11KB) - │ └── latency_test.pb.swift (12KB) - ├── Models/ - │ ├── ProtoExtensions.swift - │ ├── LatencyTracker.swift - │ └── TransportMethod.swift - ├── Views/ ✅ NEW! - │ ├── MetricCard.swift (metric display cards) - │ ├── ColorPreview.swift (color circles) - │ ├── TransportBadge.swift (transport badges) - │ ├── ConnectionStatusView.swift (health footer) - │ ├── LatencyGraph.swift (Charts) - │ └── MessageHistoryRow.swift (log rows) - └── Utilities/ - ├── Color+Components.swift - └── Date+Milliseconds.swift - -Documentation/ - ├── IMPLEMENTATION_PLAN.md - └── RESUME_HERE.md (this file) - -Scripts/ - └── generate-protos.sh ✅ UPDATED (uses Mint) - -Mintfile ✅ NEW! -Package.swift -Package.resolved ✅ NEW! -README.md -``` - ---- - -## 📋 Next Steps (Phase 3) - -### 1. Create Combine App Structure - -Create in `Sources/SundialDemoCombine/`: - -```bash -mkdir -p Sources/SundialDemoCombine/{App,ViewModels,Views} -``` - -#### App Entry Point -```swift -// Sources/SundialDemoCombine/App/SundialApp.swift -@main -struct SundialApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} -``` - -#### Main TabView -```swift -// Sources/SundialDemoCombine/App/ContentView.swift -struct ContentView: View { - var body: some View { - TabView { - MessageLabView() - .tabItem { Label("Transport", systemImage: "arrow.left.arrow.right") } - - LatencyDashboardView() - .tabItem { Label("Latency", systemImage: "clock") } - - ProtocolComparisonView() - .tabItem { Label("Protocol", systemImage: "chart.bar") } - - DiagnosticsView() - .tabItem { Label("Diagnostics", systemImage: "wrench") } - } - } -} -``` - -### 2. Implement Tab 1 - Message Transport Lab - -Create in `Sources/SundialDemoCombine/`: - -``` -App/ - ├── SundialApp.swift # @main entry point - └── ContentView.swift # TabView with 4 tabs - -ViewModels/ - └── MessageLabViewModel.swift - -Views/ - └── MessageLabView.swift -``` - -**MessageLabView** sections: -- Payload builder (complexity slider) -- Transport method selector -- Send button -- Sent/received display - ---- - -## 🎯 Key Architectural Decisions - -### 1. Direct BinaryMessagable Conformance (No Wrappers!) - -```swift -// ✅ Good - Direct conformance -extension Sundial_Demo_ColorMessage: BinaryMessagable { - public init(from data: Data) throws { - try self.init(serializedData: data) - } - - public func encode() throws -> Data { - try serializedData() - } -} - -// ❌ Bad - Unnecessary wrapper -struct ColorWrapper: BinaryMessagable { - private var proto: ColorMessage - // Extra boilerplate... -} -``` - -### 2. Transport Method Selection - -```swift -// Automatic selection based on reachability -let method = isReachable ? .sendMessage : .updateApplicationContext - -// Manual override for testing -let method = userSelection ?? autoSelected -``` - -### 3. Latency Measurement Pattern - -```swift -let sendTime = Date() -let encodeStart = Date() -let data = try message.encode() -let encodeTime = Date().timeIntervalSince(encodeStart) - -let reply = try await observer.sendMessage(data) - -let decodeStart = Date() -let decoded = try Reply(from: reply) -let decodeTime = Date().timeIntervalSince(decodeStart) - -latencyTracker.record( - sendTime: sendTime, - receiveTime: Date(), - encodeTime: encodeTime, - decodeTime: decodeTime, - ... -) -``` - ---- - -## 📚 Reference Documentation - -1. **IMPLEMENTATION_PLAN.md** - Full implementation plan with all phases -2. **README.md** - Project overview and build instructions -3. **Package.swift** - Dependencies and target configuration -4. **CLAUDE.md** (project root) - SundialKit architecture and usage -5. **ProtoExtensions.swift** - BinaryMessagable conformance examples - ---- - -## 🎓 Understanding the Demo Focus - -This demo is **NOT** a general WatchConnectivity showcase. Focus areas: - -### Primary Goals -1. **Binary Protobuf Messaging** - - Show size reduction (75%+ vs dictionary) - - Measure encoding/decoding speed - - Demonstrate complex nested messages - -2. **Latency Measurement** - - Track round-trip times - - Break down into encode/network/decode - - Compare transport methods - -3. **Transport Route Comparison** - - sendMessage (interactive, requires reachability) - - updateApplicationContext (queued, no reply) - - sendMessageData (binary-only) - -### What To Build (4 Tabs) -- Tab 1: Message Transport Lab (build/send messages) -- Tab 2: Latency Dashboard (RTT metrics and graphs) -- Tab 3: Protocol Comparison (binary vs dictionary) -- Tab 4: Live Diagnostics (connection health, logs) - ---- - -## 🚦 Build & Test Commands - -```bash -# Generate protobuf code -cd Examples/Sundial -./Scripts/generate-protos.sh - -# Build -swift build - -# Run Combine variant -swift run SundialDemoCombine - -# Run Stream variant (later) -swift run SundialDemoStream - -# Test -swift test -``` - ---- - -## ✅ Success Criteria - -- [ ] Protobuf encoding shows 75%+ size reduction -- [ ] Latency measurements accurate within 5ms -- [ ] All transport methods work correctly -- [ ] Complex messages with arrays/nested types work -- [ ] Connection transitions handled gracefully -- [ ] Both Combine and Stream variants identical -- [ ] UI clearly explains what's happening - ---- - -## 💡 Quick Start (Next Session) - -```bash -cd Examples/Sundial - -# 1. Create Combine app structure -mkdir -p Sources/SundialDemoCombine/{App,ViewModels,Views} - -# 2. Create app entry point -touch Sources/SundialDemoCombine/App/SundialApp.swift -# Implement @main App struct - -# 3. Create TabView container -touch Sources/SundialDemoCombine/App/ContentView.swift -# Implement TabView with 4 tabs - -# 4. Implement Tab 1: Message Transport Lab -touch Sources/SundialDemoCombine/ViewModels/MessageLabViewModel.swift -touch Sources/SundialDemoCombine/Views/MessageLabView.swift -# Use ConnectivityObserver from SundialKitCombine -# Use shared components: ColorPreview, TransportBadge, MetricCard - -# 5. Test build -swift build -``` - ---- - -**Status**: Ready for Phase 3 development -**Blockers**: None -**Dependencies**: ✅ All Phase 1 & 2 deliverables complete diff --git a/Examples/Sundial/Fastlane/Fastfile b/Examples/Sundial/Fastlane/Fastfile index 226646b6..1be3b38a 100644 --- a/Examples/Sundial/Fastlane/Fastfile +++ b/Examples/Sundial/Fastlane/Fastfile @@ -35,7 +35,7 @@ platform :ios do api_key = app_store_connect_api_key( key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'], issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'], - key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY'], + key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY_BASE64'], is_key_content_base64: true ) @@ -113,7 +113,7 @@ platform :ios do api_key = app_store_connect_api_key( key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'], issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'], - key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY'], + key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY_BASE64'], is_key_content_base64: true ) diff --git a/Examples/Sundial/Mintfile b/Examples/Sundial/Mintfile deleted file mode 100644 index 0e86cdcf..00000000 --- a/Examples/Sundial/Mintfile +++ /dev/null @@ -1 +0,0 @@ -apple/swift-protobuf@1.28.2 diff --git a/Examples/Sundial/Scripts/generate-protos.sh b/Examples/Sundial/Scripts/generate-protos.sh index 3376b439..ac4ce6b9 100755 --- a/Examples/Sundial/Scripts/generate-protos.sh +++ b/Examples/Sundial/Scripts/generate-protos.sh @@ -23,42 +23,25 @@ OUTPUT_DIR="$PROJECT_DIR/Sources/Shared/Generated" echo -e "${GREEN}Generating Swift code from Protocol Buffers...${NC}" -# Detect OS and set Mint path -if [ "$(uname)" = "Darwin" ]; then - DEFAULT_MINT_PATH="/opt/homebrew/bin/mint" -elif [ "$(uname)" = "Linux" ] && [ -n "$GITHUB_ACTIONS" ]; then - DEFAULT_MINT_PATH="$GITHUB_WORKSPACE/Mint/.mint/bin/mint" -elif [ "$(uname)" = "Linux" ]; then - DEFAULT_MINT_PATH="/usr/local/bin/mint" -else - echo -e "${RED}Unsupported operating system${NC}" - exit 1 -fi - -# Use environment MINT_CMD if set, otherwise use default path -MINT_CMD=${MINT_CMD:-$DEFAULT_MINT_PATH} - -# Check if Mint is installed -if ! command -v "$MINT_CMD" &> /dev/null; then - echo -e "${RED}Error: Mint not found at $MINT_CMD${NC}" - echo "Install with: brew install mint" +# Check if mise is installed +if ! command -v mise &> /dev/null; then + echo -e "${RED}Error: mise not found${NC}" + echo "Install mise: https://mise.jdx.dev/getting-started.html" exit 1 fi -# Set up Mint environment -export MINT_PATH="$PROJECT_DIR/.mint" -MINT_ARGS="-n -m $PROJECT_DIR/Mintfile --silent" -MINT_RUN="$MINT_CMD run $MINT_ARGS" +# Install pinned tools (reads .mise.toml). swift-protobuf provides both +# protoc and the protoc-gen-swift plugin, so no system protoc is required. +echo "Installing swift-protobuf via mise..." +(cd "$PROJECT_DIR" && mise install) -# Bootstrap Mint packages -echo "Installing swift-protobuf via Mint..." -$MINT_CMD bootstrap -m "$PROJECT_DIR/Mintfile" +# Expose the mise-managed bin dir (contains protoc and protoc-gen-swift) +PLUGIN_DIR="$( cd "$PROJECT_DIR" && dirname "$(mise which protoc-gen-swift)" )" -# Check if protoc is installed -if ! command -v protoc &> /dev/null; then - echo -e "${RED}Error: protoc not found${NC}" - echo "Install with: brew install protobuf" - echo "Or download from: https://github.com/protocolbuffers/protobuf/releases" +# Verify protoc is available via mise +if [ ! -x "$PLUGIN_DIR/protoc" ]; then + echo -e "${RED}Error: protoc not found in mise tools at $PLUGIN_DIR${NC}" + echo "Ensure swift-protobuf is installed: (cd $PROJECT_DIR && mise install)" exit 1 fi @@ -70,10 +53,10 @@ echo "Input: $PROTO_DIR" echo "Output: $OUTPUT_DIR" echo "" -# Run protoc with Mint-managed plugin in PATH -# Mint installs protoc-gen-swift to .mint/bin/ which protoc will find via PATH +# Run protoc with the mise-managed plugin in PATH +# protoc discovers protoc-gen-swift via PATH # Visibility=Public ensures generated types are accessible from other modules -PATH="$MINT_PATH/bin:$PATH" protoc \ +PATH="$PLUGIN_DIR:$PATH" protoc \ --swift_out=Visibility=Public:"$OUTPUT_DIR" \ --proto_path="$PROTO_DIR" \ "$PROTO_DIR"/*.proto diff --git a/Sources/SundialKitConnectivity/WatchConnectivitySession+ConnectivitySession.swift b/Sources/SundialKitConnectivity/WatchConnectivitySession+ConnectivitySession.swift index 21dfeee2..a5e3854a 100644 --- a/Sources/SundialKitConnectivity/WatchConnectivitySession+ConnectivitySession.swift +++ b/Sources/SundialKitConnectivity/WatchConnectivitySession+ConnectivitySession.swift @@ -137,11 +137,22 @@ /// /// Must be called before any message exchange can occur. /// + /// Registering the WCSession delegate happens here rather than in `init` so that + /// only the activated instance owns `WCSession.default`'s delegate; a throwaway + /// instance can never hijack delivery from the real one. Calling `activate()` more + /// than once on the same instance is safe — re-assigning `session.delegate = self` + /// is a no-op and WCSession tolerates a repeated `activate()`. + /// /// - Throws: `SundialError.sessionNotSupported` if WatchConnectivity is not supported public func activate() throws { guard WCSession.isSupported() else { throw SundialError.sessionNotSupported } + // Register as the WCSession delegate here (not in `init`) so only the + // activated instance owns `WCSession.default`'s delegate. This prevents + // throwaway instances — e.g. created by SwiftUI re-evaluating a `@State` + // autoclosure — from hijacking delivery from the real instance. + session.delegate = self session.activate() } } diff --git a/Sources/SundialKitConnectivity/WatchConnectivitySession+WCSessionDelegate.swift b/Sources/SundialKitConnectivity/WatchConnectivitySession+WCSessionDelegate.swift index 06e9610a..48a065e1 100644 --- a/Sources/SundialKitConnectivity/WatchConnectivitySession+WCSessionDelegate.swift +++ b/Sources/SundialKitConnectivity/WatchConnectivitySession+WCSessionDelegate.swift @@ -100,8 +100,27 @@ ) { // WatchConnectivity only supports property list types which are inherently Sendable let sendableMessage = ConnectivityMessage(forceCasting: message) - let handler = unsafeBitCast(replyHandler, to: ConnectivityHandler.self) + // Guard the WCSession reply handler so it can fire at most once. The same + // closure is handed to the delegate *and* auto-acknowledged below; a delegate + // that actually replies (e.g. via `ConnectivityReceiveContext.replyWith`) would + // otherwise invoke `replyHandler` twice, which is undefined behavior on the + // sender side. + var replied = false + let safeReply: ([String: Any]) -> Void = { response in + guard !replied else { + return + } + replied = true + replyHandler(response) + } + let handler = unsafeBitCast(safeReply, to: ConnectivityHandler.self) delegate?.session(self, didReceiveMessage: sendableMessage, replyHandler: handler) + // Auto-acknowledge so the sender's reply-expecting `sendMessage` completes + // immediately instead of timing out (WCErrorDomain 7012). Skipped if the + // delegate already replied. Mirrors the binary `didReceiveMessageData` path. + if !replied { + replyHandler([:]) + } } internal func session( @@ -136,9 +155,23 @@ didReceiveMessageData messageData: Data, replyHandler: @escaping (Data) -> Void ) { - let handler = unsafeBitCast(replyHandler, to: (@Sendable (Data) -> Void).self) + // Guard the WCSession reply handler so it can fire at most once. Unlike the + // dictionary path, the `ConnectivityDelegateHandling` bridge forwards this + // handler to consumers, so a delegate that replies plus the unconditional + // auto-acknowledgment below would invoke `replyHandler` twice. + var replied = false + let safeReply: (Data) -> Void = { response in + guard !replied else { + return + } + replied = true + replyHandler(response) + } + let handler = unsafeBitCast(safeReply, to: (@Sendable (Data) -> Void).self) delegate?.session(self, didReceiveMessageData: messageData, replyHandler: handler) - replyHandler(Data()) + if !replied { + replyHandler(Data()) + } } } diff --git a/Sources/SundialKitConnectivity/WatchConnectivitySession.swift b/Sources/SundialKitConnectivity/WatchConnectivitySession.swift index 5600c61f..5857975b 100644 --- a/Sources/SundialKitConnectivity/WatchConnectivitySession.swift +++ b/Sources/SundialKitConnectivity/WatchConnectivitySession.swift @@ -71,7 +71,11 @@ internal init(session: WCSession) { self.session = session super.init() - session.delegate = self + // The WCSession delegate is registered in `activate()`, not here, so that + // constructing a `WatchConnectivitySession` has no global side effect. A + // throwaway instance (e.g. one created then immediately discarded by a + // SwiftUI `@State` autoclosure re-evaluation) must not hijack + // `WCSession.default`'s delegate from the active, activated instance. } override public convenience init() {