Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
637
examples/wasm/ios/swift/RuvectorWasm.swift
Normal file
637
examples/wasm/ios/swift/RuvectorWasm.swift
Normal file
@@ -0,0 +1,637 @@
|
||||
//
|
||||
// RuvectorWasm.swift
|
||||
// Privacy-Preserving On-Device AI for iOS
|
||||
//
|
||||
// Uses WasmKit to run Ruvector WASM directly on iOS
|
||||
// Minimum iOS: 15.0 (WasmKit requirement)
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Core Types
|
||||
|
||||
/// Distance metric for vector similarity
|
||||
public enum DistanceMetric: UInt8 {
|
||||
case euclidean = 0
|
||||
case cosine = 1
|
||||
case manhattan = 2
|
||||
case dotProduct = 3
|
||||
}
|
||||
|
||||
/// Quantization mode for memory optimization
|
||||
public enum QuantizationMode: UInt8 {
|
||||
case none = 0
|
||||
case scalar = 1 // 4x compression
|
||||
case binary = 2 // 32x compression
|
||||
case product = 3 // Variable compression
|
||||
}
|
||||
|
||||
/// Search result with vector ID and distance
|
||||
public struct SearchResult: Identifiable {
|
||||
public let id: UInt64
|
||||
public let distance: Float
|
||||
}
|
||||
|
||||
// MARK: - Health Learning Types
|
||||
|
||||
/// Health metric types (privacy-preserving)
|
||||
public enum HealthMetricType: UInt8 {
|
||||
case heartRate = 0
|
||||
case steps = 1
|
||||
case sleep = 2
|
||||
case activeEnergy = 3
|
||||
case exerciseMinutes = 4
|
||||
case standHours = 5
|
||||
case distance = 6
|
||||
case flightsClimbed = 7
|
||||
case mindfulness = 8
|
||||
case respiratoryRate = 9
|
||||
case bloodOxygen = 10
|
||||
case hrv = 11
|
||||
}
|
||||
|
||||
/// Health state for learning (no actual values stored)
|
||||
public struct HealthState {
|
||||
public let metric: HealthMetricType
|
||||
public let valueBucket: UInt8 // 0-9 normalized
|
||||
public let hour: UInt8
|
||||
public let dayOfWeek: UInt8
|
||||
|
||||
public init(metric: HealthMetricType, valueBucket: UInt8, hour: UInt8, dayOfWeek: UInt8) {
|
||||
self.metric = metric
|
||||
self.valueBucket = min(valueBucket, 9)
|
||||
self.hour = min(hour, 23)
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Location Learning Types
|
||||
|
||||
/// Location categories (no coordinates stored)
|
||||
public enum LocationCategory: UInt8 {
|
||||
case home = 0
|
||||
case work = 1
|
||||
case gym = 2
|
||||
case dining = 3
|
||||
case shopping = 4
|
||||
case transit = 5
|
||||
case outdoor = 6
|
||||
case entertainment = 7
|
||||
case healthcare = 8
|
||||
case education = 9
|
||||
case unknown = 10
|
||||
}
|
||||
|
||||
/// Location state for learning
|
||||
public struct LocationState {
|
||||
public let category: LocationCategory
|
||||
public let hour: UInt8
|
||||
public let dayOfWeek: UInt8
|
||||
public let durationMinutes: UInt16
|
||||
|
||||
public init(category: LocationCategory, hour: UInt8, dayOfWeek: UInt8, durationMinutes: UInt16) {
|
||||
self.category = category
|
||||
self.hour = min(hour, 23)
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
self.durationMinutes = durationMinutes
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Communication Learning Types
|
||||
|
||||
/// Communication event types
|
||||
public enum CommEventType: UInt8 {
|
||||
case callIncoming = 0
|
||||
case callOutgoing = 1
|
||||
case messageReceived = 2
|
||||
case messageSent = 3
|
||||
case emailReceived = 4
|
||||
case emailSent = 5
|
||||
case notification = 6
|
||||
}
|
||||
|
||||
/// Communication state
|
||||
public struct CommState {
|
||||
public let eventType: CommEventType
|
||||
public let hour: UInt8
|
||||
public let dayOfWeek: UInt8
|
||||
public let responseTimeBucket: UInt8 // 0-9 normalized
|
||||
|
||||
public init(eventType: CommEventType, hour: UInt8, dayOfWeek: UInt8, responseTimeBucket: UInt8) {
|
||||
self.eventType = eventType
|
||||
self.hour = min(hour, 23)
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
self.responseTimeBucket = min(responseTimeBucket, 9)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Calendar Learning Types
|
||||
|
||||
/// Calendar event types
|
||||
public enum CalendarEventType: UInt8 {
|
||||
case meeting = 0
|
||||
case focusTime = 1
|
||||
case personal = 2
|
||||
case travel = 3
|
||||
case breakTime = 4
|
||||
case exercise = 5
|
||||
case social = 6
|
||||
case deadline = 7
|
||||
}
|
||||
|
||||
/// Calendar event for learning
|
||||
public struct CalendarEvent {
|
||||
public let eventType: CalendarEventType
|
||||
public let startHour: UInt8
|
||||
public let durationMinutes: UInt16
|
||||
public let dayOfWeek: UInt8
|
||||
public let isRecurring: Bool
|
||||
public let hasAttendees: Bool
|
||||
|
||||
public init(eventType: CalendarEventType, startHour: UInt8, durationMinutes: UInt16,
|
||||
dayOfWeek: UInt8, isRecurring: Bool, hasAttendees: Bool) {
|
||||
self.eventType = eventType
|
||||
self.startHour = min(startHour, 23)
|
||||
self.durationMinutes = durationMinutes
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
self.isRecurring = isRecurring
|
||||
self.hasAttendees = hasAttendees
|
||||
}
|
||||
}
|
||||
|
||||
/// Time slot pattern
|
||||
public struct TimeSlotPattern {
|
||||
public let busyProbability: Float
|
||||
public let avgMeetingDuration: Float
|
||||
public let focusScore: Float
|
||||
public let eventCount: UInt32
|
||||
}
|
||||
|
||||
/// Focus time suggestion
|
||||
public struct FocusTimeSuggestion: Identifiable {
|
||||
public var id: String { "\(day)-\(startHour)" }
|
||||
public let day: UInt8
|
||||
public let startHour: UInt8
|
||||
public let score: Float
|
||||
}
|
||||
|
||||
// MARK: - App Usage Learning Types
|
||||
|
||||
/// App categories
|
||||
public enum AppCategory: UInt8 {
|
||||
case social = 0
|
||||
case productivity = 1
|
||||
case entertainment = 2
|
||||
case news = 3
|
||||
case communication = 4
|
||||
case health = 5
|
||||
case navigation = 6
|
||||
case shopping = 7
|
||||
case gaming = 8
|
||||
case education = 9
|
||||
case finance = 10
|
||||
case utilities = 11
|
||||
}
|
||||
|
||||
/// App usage session
|
||||
public struct AppUsageSession {
|
||||
public let category: AppCategory
|
||||
public let durationSeconds: UInt32
|
||||
public let hour: UInt8
|
||||
public let dayOfWeek: UInt8
|
||||
public let isActiveUse: Bool
|
||||
|
||||
public init(category: AppCategory, durationSeconds: UInt32, hour: UInt8,
|
||||
dayOfWeek: UInt8, isActiveUse: Bool) {
|
||||
self.category = category
|
||||
self.durationSeconds = durationSeconds
|
||||
self.hour = min(hour, 23)
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
self.isActiveUse = isActiveUse
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen time summary
|
||||
public struct ScreenTimeSummary {
|
||||
public let totalMinutes: Float
|
||||
public let topCategory: AppCategory
|
||||
public let byCategory: [AppCategory: Float]
|
||||
}
|
||||
|
||||
/// Wellbeing insight
|
||||
public struct WellbeingInsight: Identifiable {
|
||||
public var id: String { category }
|
||||
public let category: String
|
||||
public let message: String
|
||||
public let score: Float
|
||||
}
|
||||
|
||||
// MARK: - iOS Context & Recommendations
|
||||
|
||||
/// Device context for recommendations
|
||||
public struct IOSContext {
|
||||
public let hour: UInt8
|
||||
public let dayOfWeek: UInt8
|
||||
public let isWeekend: Bool
|
||||
public let batteryLevel: UInt8 // 0-100
|
||||
public let networkType: UInt8 // 0=none, 1=wifi, 2=cellular
|
||||
public let locationCategory: LocationCategory
|
||||
public let recentAppCategory: AppCategory
|
||||
public let activityLevel: UInt8 // 0-10
|
||||
public let healthScore: Float // 0-1
|
||||
|
||||
public init(hour: UInt8, dayOfWeek: UInt8, batteryLevel: UInt8 = 100,
|
||||
networkType: UInt8 = 1, locationCategory: LocationCategory = .unknown,
|
||||
recentAppCategory: AppCategory = .utilities, activityLevel: UInt8 = 5,
|
||||
healthScore: Float = 0.5) {
|
||||
self.hour = min(hour, 23)
|
||||
self.dayOfWeek = min(dayOfWeek, 6)
|
||||
self.isWeekend = dayOfWeek == 0 || dayOfWeek == 6
|
||||
self.batteryLevel = min(batteryLevel, 100)
|
||||
self.networkType = min(networkType, 2)
|
||||
self.locationCategory = locationCategory
|
||||
self.recentAppCategory = recentAppCategory
|
||||
self.activityLevel = min(activityLevel, 10)
|
||||
self.healthScore = min(max(healthScore, 0), 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Activity suggestion
|
||||
public struct ActivitySuggestion: Identifiable {
|
||||
public var id: String { category }
|
||||
public let category: String
|
||||
public let confidence: Float
|
||||
public let reason: String
|
||||
}
|
||||
|
||||
/// Context-aware recommendations
|
||||
public struct ContextRecommendations {
|
||||
public let suggestedAppCategory: AppCategory
|
||||
public let focusScore: Float
|
||||
public let activitySuggestions: [ActivitySuggestion]
|
||||
public let optimalNotificationTime: Bool
|
||||
}
|
||||
|
||||
// MARK: - WASM Runtime Error
|
||||
|
||||
/// WASM runtime errors
|
||||
public enum RuvectorError: Error, LocalizedError {
|
||||
case wasmNotLoaded
|
||||
case initializationFailed(String)
|
||||
case memoryAllocationFailed
|
||||
case invalidInput(String)
|
||||
case serializationFailed
|
||||
case deserializationFailed
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .wasmNotLoaded:
|
||||
return "WASM module not loaded"
|
||||
case .initializationFailed(let msg):
|
||||
return "Initialization failed: \(msg)"
|
||||
case .memoryAllocationFailed:
|
||||
return "Memory allocation failed"
|
||||
case .invalidInput(let msg):
|
||||
return "Invalid input: \(msg)"
|
||||
case .serializationFailed:
|
||||
return "Serialization failed"
|
||||
case .deserializationFailed:
|
||||
return "Deserialization failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ruvector WASM Runtime
|
||||
|
||||
/// Main entry point for Ruvector WASM on iOS
|
||||
/// Uses WasmKit for native WASM execution
|
||||
public final class RuvectorWasm {
|
||||
|
||||
/// Shared instance (singleton pattern for resource efficiency)
|
||||
public static let shared = RuvectorWasm()
|
||||
|
||||
// WASM runtime state
|
||||
private var isLoaded = false
|
||||
private var wasmBytes: Data?
|
||||
private var memoryPtr: UnsafeMutableRawPointer?
|
||||
private var memorySize: Int = 0
|
||||
|
||||
// Learning state handles
|
||||
private var healthLearnerHandle: Int32 = -1
|
||||
private var locationLearnerHandle: Int32 = -1
|
||||
private var commLearnerHandle: Int32 = -1
|
||||
private var calendarLearnerHandle: Int32 = -1
|
||||
private var appUsageLearnerHandle: Int32 = -1
|
||||
private var iosLearnerHandle: Int32 = -1
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Load WASM module from bundle
|
||||
/// - Parameter bundlePath: Path to .wasm file in app bundle
|
||||
public func load(from bundlePath: String) throws {
|
||||
guard let data = FileManager.default.contents(atPath: bundlePath) else {
|
||||
throw RuvectorError.initializationFailed("WASM file not found at \(bundlePath)")
|
||||
}
|
||||
try load(wasmData: data)
|
||||
}
|
||||
|
||||
/// Load WASM module from data
|
||||
/// - Parameter wasmData: Raw WASM bytes
|
||||
public func load(wasmData: Data) throws {
|
||||
self.wasmBytes = wasmData
|
||||
|
||||
// In production: Initialize WasmKit runtime here
|
||||
// For now, mark as loaded for API design
|
||||
// TODO: Integrate WasmKit when added as dependency
|
||||
//
|
||||
// Example WasmKit integration:
|
||||
// let module = try WasmKit.Module(bytes: [UInt8](wasmData))
|
||||
// let instance = try module.instantiate()
|
||||
// self.wasmInstance = instance
|
||||
|
||||
isLoaded = true
|
||||
}
|
||||
|
||||
/// Check if WASM is loaded
|
||||
public var isReady: Bool { isLoaded }
|
||||
|
||||
// MARK: - Memory Management
|
||||
|
||||
/// Allocate memory in WASM linear memory
|
||||
private func allocate(size: Int) throws -> Int {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call wasm_alloc export
|
||||
return 0
|
||||
}
|
||||
|
||||
/// Free memory in WASM linear memory
|
||||
private func free(ptr: Int, size: Int) throws {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call wasm_free export
|
||||
}
|
||||
|
||||
// MARK: - SIMD Operations
|
||||
|
||||
/// Compute dot product of two vectors
|
||||
public func dotProduct(_ a: [Float], _ b: [Float]) throws -> Float {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
guard a.count == b.count else {
|
||||
throw RuvectorError.invalidInput("Vectors must have same length")
|
||||
}
|
||||
|
||||
// Pure Swift fallback (SIMD when available)
|
||||
var result: Float = 0
|
||||
for i in 0..<a.count {
|
||||
result += a[i] * b[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Compute L2 distance
|
||||
public func l2Distance(_ a: [Float], _ b: [Float]) throws -> Float {
|
||||
guard a.count == b.count else {
|
||||
throw RuvectorError.invalidInput("Vectors must have same length")
|
||||
}
|
||||
|
||||
var sum: Float = 0
|
||||
for i in 0..<a.count {
|
||||
let diff = a[i] - b[i]
|
||||
sum += diff * diff
|
||||
}
|
||||
return sqrt(sum)
|
||||
}
|
||||
|
||||
/// Compute cosine similarity
|
||||
public func cosineSimilarity(_ a: [Float], _ b: [Float]) throws -> Float {
|
||||
guard a.count == b.count else {
|
||||
throw RuvectorError.invalidInput("Vectors must have same length")
|
||||
}
|
||||
|
||||
var dot: Float = 0
|
||||
var normA: Float = 0
|
||||
var normB: Float = 0
|
||||
|
||||
for i in 0..<a.count {
|
||||
dot += a[i] * b[i]
|
||||
normA += a[i] * a[i]
|
||||
normB += b[i] * b[i]
|
||||
}
|
||||
|
||||
let denom = sqrt(normA) * sqrt(normB)
|
||||
return denom > 0 ? dot / denom : 0
|
||||
}
|
||||
|
||||
// MARK: - iOS Learner (Unified)
|
||||
|
||||
/// Initialize unified iOS learner
|
||||
public func initIOSLearner() throws {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_learner_init export
|
||||
iosLearnerHandle = 0
|
||||
}
|
||||
|
||||
/// Update health metrics
|
||||
public func updateHealth(_ state: HealthState) throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_update_health export
|
||||
}
|
||||
|
||||
/// Update location
|
||||
public func updateLocation(_ state: LocationState) throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_update_location export
|
||||
}
|
||||
|
||||
/// Update communication patterns
|
||||
public func updateCommunication(_ state: CommState) throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_update_communication export
|
||||
}
|
||||
|
||||
/// Update calendar
|
||||
public func updateCalendar(_ event: CalendarEvent) throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_update_calendar export
|
||||
}
|
||||
|
||||
/// Update app usage
|
||||
public func updateAppUsage(_ session: AppUsageSession) throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_update_app_usage export
|
||||
}
|
||||
|
||||
/// Get context-aware recommendations
|
||||
public func getRecommendations(_ context: IOSContext) throws -> ContextRecommendations {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
|
||||
// TODO: Call ios_get_recommendations export
|
||||
// For now, return sensible defaults
|
||||
return ContextRecommendations(
|
||||
suggestedAppCategory: .productivity,
|
||||
focusScore: 0.7,
|
||||
activitySuggestions: [
|
||||
ActivitySuggestion(category: "Focus", confidence: 0.8, reason: "Good time for deep work")
|
||||
],
|
||||
optimalNotificationTime: context.hour >= 9 && context.hour <= 18
|
||||
)
|
||||
}
|
||||
|
||||
/// Train one iteration (call periodically)
|
||||
public func trainIteration() throws {
|
||||
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call ios_train export
|
||||
}
|
||||
|
||||
// MARK: - Calendar Learning
|
||||
|
||||
/// Initialize calendar learner
|
||||
public func initCalendarLearner() throws {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call calendar_init export
|
||||
calendarLearnerHandle = 0
|
||||
}
|
||||
|
||||
/// Learn from calendar event
|
||||
public func learnCalendarEvent(_ event: CalendarEvent) throws {
|
||||
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call calendar_learn_event export
|
||||
}
|
||||
|
||||
/// Get busy probability for time slot
|
||||
public func calendarBusyProbability(hour: UInt8, dayOfWeek: UInt8) throws -> Float {
|
||||
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call calendar_is_busy export
|
||||
return 0.5
|
||||
}
|
||||
|
||||
/// Suggest focus times
|
||||
public func suggestFocusTimes(durationHours: UInt8) throws -> [FocusTimeSuggestion] {
|
||||
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call through WASM
|
||||
return [
|
||||
FocusTimeSuggestion(day: 1, startHour: 9, score: 0.9),
|
||||
FocusTimeSuggestion(day: 2, startHour: 14, score: 0.85)
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - App Usage Learning
|
||||
|
||||
/// Initialize app usage learner
|
||||
public func initAppUsageLearner() throws {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call app_usage_init export
|
||||
appUsageLearnerHandle = 0
|
||||
}
|
||||
|
||||
/// Learn from app session
|
||||
public func learnAppSession(_ session: AppUsageSession) throws {
|
||||
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call app_usage_learn export
|
||||
}
|
||||
|
||||
/// Get screen time (hours)
|
||||
public func screenTime() throws -> Float {
|
||||
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call app_usage_screen_time export
|
||||
return 2.5
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
/// Serialize all learning state
|
||||
public func serialize() throws -> Data {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call serialize exports for each learner
|
||||
return Data()
|
||||
}
|
||||
|
||||
/// Deserialize learning state
|
||||
public func deserialize(_ data: Data) throws {
|
||||
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
||||
// TODO: Call deserialize exports
|
||||
}
|
||||
|
||||
/// Save state to file
|
||||
public func save(to url: URL) throws {
|
||||
let data = try serialize()
|
||||
try data.write(to: url)
|
||||
}
|
||||
|
||||
/// Load state from file
|
||||
public func restore(from url: URL) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
try deserialize(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Integration
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
/// Observable wrapper for SwiftUI
|
||||
@available(iOS 15.0, macOS 12.0, *)
|
||||
@MainActor
|
||||
public final class RuvectorViewModel: ObservableObject {
|
||||
@Published public private(set) var isReady = false
|
||||
@Published public private(set) var recommendations: ContextRecommendations?
|
||||
@Published public private(set) var screenTimeHours: Float = 0
|
||||
@Published public private(set) var focusScore: Float = 0
|
||||
|
||||
private let runtime = RuvectorWasm.shared
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Load WASM module
|
||||
public func load(from bundlePath: String) async throws {
|
||||
try runtime.load(from: bundlePath)
|
||||
try runtime.initIOSLearner()
|
||||
try runtime.initCalendarLearner()
|
||||
try runtime.initAppUsageLearner()
|
||||
isReady = true
|
||||
}
|
||||
|
||||
/// Update recommendations for current context
|
||||
public func updateRecommendations(context: IOSContext) async throws {
|
||||
recommendations = try runtime.getRecommendations(context)
|
||||
focusScore = recommendations?.focusScore ?? 0
|
||||
}
|
||||
|
||||
/// Update screen time
|
||||
public func updateScreenTime() async throws {
|
||||
screenTimeHours = try runtime.screenTime()
|
||||
}
|
||||
|
||||
/// Record app usage
|
||||
public func recordAppUsage(_ session: AppUsageSession) async throws {
|
||||
try runtime.learnAppSession(session)
|
||||
try await updateScreenTime()
|
||||
}
|
||||
|
||||
/// Record calendar event
|
||||
public func recordCalendarEvent(_ event: CalendarEvent) async throws {
|
||||
try runtime.learnCalendarEvent(event)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Combine Integration
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
|
||||
@available(iOS 13.0, macOS 10.15, *)
|
||||
extension RuvectorWasm {
|
||||
/// Publisher for periodic training
|
||||
public func trainingPublisher(interval: TimeInterval = 60) -> AnyPublisher<Void, Never> {
|
||||
Timer.publish(every: interval, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.map { [weak self] _ in
|
||||
try? self?.trainIteration()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user