Skip to content

Latest commit

 

History

History
408 lines (327 loc) · 15.4 KB

File metadata and controls

408 lines (327 loc) · 15.4 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

SundialKit v2.0.0 is a Swift 6.1+ communications library for Apple platforms with a modern three-layer architecture:

Layer 1: Core Protocols (SundialKitCore, SundialKitNetwork, SundialKitConnectivity)

  • Protocol-based abstractions over Apple's Network and WatchConnectivity frameworks
  • Minimal concurrency annotations (Sendable constraints)
  • No observer patterns - pure wrappers

Layer 2: Observation Plugins (Choose your concurrency model)

  • SundialKitStream: Modern actor-based observers with AsyncStream APIs
  • SundialKitCombine: @MainActor-based observers with Combine publishers

Key Features:

  • Network connectivity monitoring using Apple's Network framework
  • WatchConnectivity abstraction for iPhone/Apple Watch communication
  • Multiple concurrency models: Choose actors or @MainActor based on your needs
  • Swift 6.1 strict concurrency compliant (zero @unchecked Sendable in plugins)
  • Full async/await support alongside Combine publishers

Build & Test Commands

Using Make (Recommended)

make build          # Build the package
make test           # Run tests with coverage
make lint           # Run linting and formatting (strict mode)
make format         # Format code only
make clean          # Clean build artifacts
make help           # Show all available commands

Using Swift Directly

swift build
swift test
swift test --enable-code-coverage
swift test --filter <TestName>

Formatting & Linting (Mise)

SundialKit uses mise to manage development tools:

  • swift-format (swiftlang/swift-format@602.0.0) - Official Apple Swift formatter
  • SwiftLint (realm/SwiftLint@0.61.0) - Swift style and conventions linter
  • Periphery (peripheryapp/periphery@3.2.0) - Unused code detection

Install mise (macOS)

curl https://mise.run | sh
# or
brew install mise

Install Development Tools

mise install  # Installs tools from .mise.toml

Run linting script

./Scripts/lint.sh              # Normal mode
LINT_MODE=STRICT ./Scripts/lint.sh  # Strict mode (CI)
FORMAT_ONLY=1 ./Scripts/lint.sh     # Format only

Architecture

Three-Layer Architecture (v2.0.0)

SundialKit v2.0.0 uses a layered architecture separating protocols, wrappers, and observation patterns:

┌─────────────────────────────────────────────────────────────┐
│ Layer 1: SundialKitCore (Protocols)                         │
│ - PathMonitor, NetworkPing, ConnectivitySession protocols   │
│ - Sendable-safe type aliases and errors                     │
│ - No observers, no concurrency primitives                   │
└─────────────────────────────────────────────────────────────┘
         │
    ┌────┴────┐
    │         │
┌───┴────────┐ ┌──┴──────────────┐
│ Layer 1:   │ │ Layer 1:        │
│ Network    │ │ Connectivity    │
│            │ │                 │
│ Raw        │ │ Raw             │
│ wrappers   │ │ wrappers        │
│ over       │ │ over            │
│ NWPath     │ │ WCSession       │
│ Monitor    │ │                 │
└────────────┘ └─────────────────┘
    │                  │
    └────┬─────────────┘
         │
    ┌────┴────┐
    │         │
┌───┴────────┐│       ┌──────────────┐
│ Layer 2:   ││       │ Layer 2:     │
│ Stream     ││       │ Combine      │
│            ││       │              │
│ Actors +   ││       │ @MainActor + │
│ AsyncStream││       │ Publishers   │
│ (modern)   ││       │ (SwiftUI)    │
└────────────┘│       └──────────────┘

Layer 1: Core Protocols & Wrappers

SundialKitCore (Sources/SundialKitCore/)

  • Protocol definitions: PathMonitor, NetworkPing, ConnectivitySession, ConnectivitySessionDelegate
  • Type-safe aliases: ConnectivityMessage = [String: any Sendable]
  • Typed errors: ConnectivityError, NetworkError, SerializationError
  • No concrete implementations, no observers

SundialKitNetwork (Sources/SundialKitNetwork/)

  • Concrete implementations: NWPathMonitor extensions, NeverPing
  • Protocol wrappers around Apple's Network framework
  • PathStatus: Enum representing network state (satisfied, unsatisfied, requiresConnection, unknown)
    • Contains Interface OptionSet (cellular, wifi, wiredEthernet, loopback, other)
    • Contains UnsatisfiedReason enum for failure details

SundialKitConnectivity (Sources/SundialKitConnectivity/)

  • Concrete implementations: WatchConnectivitySession, NeverConnectivitySession
  • Protocol wrappers around Apple's WatchConnectivity framework
  • Delegate pattern support via ConnectivitySessionDelegate
  • Message encoding/decoding via Messagable protocol

Layer 2: Observation Plugins (Choose One)

SundialKitStream (Packages/SundialKitStream/) - Modern Async/Await

  • NetworkObserver: Actor-based network monitoring

    • Generic over PathMonitor and NetworkPing protocols
    • AsyncStream APIs: pathUpdates(), pathStatusStream, isExpensiveStream, pingStatusStream
    • Call start(queue:) to begin monitoring
    • Zero @unchecked Sendable (naturally Sendable actors)
  • ConnectivityObserver: Actor-based WatchConnectivity

    • AsyncStream APIs: activationStates(), messageStream(), reachabilityStream()
    • Async methods: activate(), sendMessage(_:) returns ConnectivitySendResult
    • Automatic message routing (sendMessage when reachable, updateApplicationContext when not)
    • Zero @unchecked Sendable (naturally Sendable actors)

SundialKitCombine (Packages/SundialKitCombine/) - Combine + SwiftUI

  • NetworkObserver: @MainActor-based network monitoring

    • Generic over PathMonitor and NetworkPing & Sendable protocols
    • @Published properties: pathStatus, isExpensive, isConstrained, pingStatus
    • Call start(queue:) to begin monitoring (defaults to .main)
    • Zero @unchecked Sendable (@MainActor isolation)
  • ConnectivityObserver: @MainActor-based WatchConnectivity

    • @Published properties: activationState, isReachable, isPairedAppInstalled
    • PassthroughSubject publishers: messageReceived, sendResult
    • Async methods: activate(), sendMessage(_:) returns ConnectivitySendResult
    • Automatic message routing
    • Zero @unchecked Sendable (@MainActor isolation)

Message Encoding/Decoding

Messagable Protocol

  • Allows type-safe message encoding/decoding for WatchConnectivity
  • Requires: static key (type identifier), init?(from:), parameters()
  • Extension provides message() method to convert to ConnectivityMessage ([String:Any])

MessageDecoder

  • Initialize with array of Messagable.Type
  • Builds internal dictionary keyed by Messagable.key
  • Use decode(_:) to convert ConnectivityMessage back to typed Messagable

Testing Architecture

  • All tests in Tests/SundialKitTests/
  • Testing Framework: Swift Testing (requires Swift 6.1+) - v2.0.0 migration from XCTest
  • Mock implementations prefixed with "Mock" (MockSession, MockPathMonitor, MockNetworkPing, MockMessage)
  • Protocol-based abstractions enable dependency injection for testing
  • Never* types (NeverPing, NeverConnectivitySession) used for platforms without certain capabilities

Key Design Patterns

  • Protocol-oriented: Core types are protocols (PathMonitor, NetworkPing, ConnectivitySession)
  • Platform availability: Heavy use of #if canImport(Combine) and @available attributes
  • Reactive: All state changes published via Combine publishers
  • PassthroughSubject extension: Custom anyPublisher(for:) helper maps KeyPath to publisher

Platform Support

  • Swift Version: Swift 6.1+ required (v2.0.0+)
  • Deployment Targets: iOS 16, watchOS 9, tvOS 16, macOS 11.0
  • Requires Combine framework (macOS 10.15+, iOS 13+, watchOS 6+, tvOS 13+)
  • WatchConnectivity only available on iOS and watchOS (not macOS/tvOS)
  • Note: v2.0.0 dropped support for Swift 5.9, 5.10, and 6.0 to enable Swift Testing framework migration

Development Notes

Development Tools

Development tools (formatter, linter, unused code detector) are managed via mise and defined in .mise.toml. The Scripts/lint.sh script orchestrates formatting, linting, and code quality checks. Use make lint for local development.

Important Type Aliases

  • ConnectivityMessage = [String: any Sendable] (Sendable-safe WatchConnectivity dictionary)
  • ConnectivityHandler = @Sendable (ConnectivityMessage) -> Void
  • SuccessfulSubject<Output> = PassthroughSubject<Output, Never> (in SundialKitCore for legacy support)

Usage Examples

Network Monitoring with SundialKitStream (Async/Await)

import SundialKitStream
import SundialKitNetwork

// Create observer (actor-based)
let observer = NetworkObserver(
  monitor: NWPathMonitorAdapter(),
  ping: nil  // or provide a NetworkPing implementation
)

// Start monitoring
observer.start(queue: .global())

// Consume path updates using AsyncStream
Task {
  for await status in observer.pathStatusStream {
    print("Network status: \(status)")
  }
}

// Or get raw path updates
Task {
  for await path in observer.pathUpdates() {
    print("Path: \(path)")
  }
}

Network Monitoring with SundialKitCombine (Combine + SwiftUI)

import SundialKitCombine
import SundialKitNetwork
import Combine

// Create observer (@MainActor-based)
let observer = NetworkObserver(
  monitor: NWPathMonitorAdapter(),
  ping: nil
)

// Start monitoring on main queue
observer.start()

// Use @Published properties in SwiftUI
var cancellables = Set<AnyCancellable>()

observer.$pathStatus
  .sink { status in
    print("Network status: \(status)")
  }
  .store(in: &cancellables)

observer.$isExpensive
  .sink { isExpensive in
    print("Is expensive: \(isExpensive)")
  }
  .store(in: &cancellables)

WatchConnectivity with SundialKitStream (Async/Await)

import SundialKitStream
import SundialKitConnectivity

// Create observer (actor-based)
let observer = ConnectivityObserver()

// Activate session
try await observer.activate()

// Listen for messages using AsyncStream
Task {
  for await result in observer.messageStream() {
    switch result.context {
    case .replyWith(let handler):
      print("Message: \(result.message)")
      handler(["response": "acknowledged"])
    case .applicationContext:
      print("Context update: \(result.message)")
    }
  }
}

// Send messages
let result = try await observer.sendMessage(["key": "value"])
print("Sent via: \(result.context)")

WatchConnectivity with SundialKitCombine (Combine + SwiftUI)

import SundialKitCombine
import SundialKitConnectivity
import Combine

// Create observer (@MainActor-based)
let observer = ConnectivityObserver()

// Activate session
try observer.activate()

// Use publishers
var cancellables = Set<AnyCancellable>()

observer.messageReceived
  .sink { result in
    switch result.context {
    case .replyWith(let handler):
      print("Message: \(result.message)")
      handler(["response": "acknowledged"])
    case .applicationContext:
      print("Context update: \(result.message)")
    }
  }
  .store(in: &cancellables)

observer.$activationState
  .sink { state in
    print("State: \(state)")
  }
  .store(in: &cancellables)

// Send messages asynchronously
Task {
  let result = try await observer.sendMessage(["key": "value"])
  print("Sent via: \(result.context)")
}

Common Pitfalls

  • Observers require explicit start/activate: Both NetworkObserver and ConnectivityObserver need start(queue:) or activate() calls
  • Platform-specific APIs: WatchConnectivity guarded with @available and #if (behavior differs on iOS vs watchOS)
  • Messages must be property list types: ConnectivityMessage values must be Sendable property list types
  • Actor isolation: When using SundialKitStream, remember to use await for actor-isolated properties and methods
  • Main thread access: When using SundialKitCombine, all observer access is on MainActor (safe for UI updates)

Repository Structure & GitHub Workflow Integration

Package Structure (v2.0.0)

SundialKit v2.0.0 uses a modular Swift Package Manager architecture:

Repository Structure:

  • Main Repository (brightdigit/SundialKit): Contains SundialKitCore, SundialKitNetwork, SundialKitConnectivity, SundialKit umbrella, and built-in features (SundialKitBinary, SundialKitMessagable)
  • Plugin Packages: SundialKitStream and SundialKitCombine are distributed as separate Swift packages
    • brightdigit/SundialKitStream (tag: 1.0.0-alpha.1) - Modern async/await observers
    • brightdigit/SundialKitCombine (tag: 1.0.0-alpha.1) - Combine-based observers

Package Dependencies:

SundialKit uses standard Swift Package Manager dependencies. The Package.swift file references plugin packages as remote dependencies:

dependencies: [
    .package(url: "https://github.com/brightdigit/SundialKitStream.git", from: "1.0.0-alpha.1"),
    .package(url: "https://github.com/brightdigit/SundialKitCombine.git", from: "1.0.0-alpha.1")
]

Working with Dependencies:

# Update all package dependencies
swift package update

# Resolve dependencies
swift package resolve

# Clean and rebuild
swift package clean
swift build

GitHub Issues & Pull Requests

This project uses GitHub Issues and Pull Requests with component-based organization:

  • Each major feature gets a GitHub issue and feature branch
  • Subtasks are tracked as task lists in the issue or as sub-issues
  • Component/Package labeling is required for all issues and PRs
    • Issue titles prefixed with component: [Core] Feature: ..., [Network] Feature: ..., [WatchConnectivity] Feature: ...
    • GitHub labels applied: component:core, component:network, component:watchconnectivity, component:combine, component:stream, etc.
  • Feature branches follow the pattern: feature/[component-]<description>
  • Commit messages reference component: feat(core): description (#issue-number)
  • Pull requests include component scope: feat(core): Feature Description
  • Pull requests are created when work is complete, closing the related issue

Component Labels:

  • component:core - SundialKitCore protocols and types
  • component:network - SundialKitNetwork implementation
  • component:watchconnectivity - SundialKitConnectivity implementation
  • component:combine - SundialKitCombine plugin package (v1 compatibility)
  • component:messagable - SundialKitMessagable built-in (v1 compatibility)
  • component:stream - SundialKitStream plugin package (modern async/await)
  • component:binary - SundialKitBinary built-in (modern serialization)
  • component:infrastructure - Build, CI/CD, tooling, package management
  • component:docs - Documentation and examples
  • component:tests - Testing infrastructure and Swift Testing migration