Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DittoSyncStatusHelper tool #162

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
12 changes: 11 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ let package = Package(
.library(
name: "DittoPermissionsHealth",
targets: ["DittoPermissionsHealth"]),
.library(
name: "DittoSyncStatusHelper",
targets: ["DittoSyncStatusHelper"]),
.library(
name: "DittoAllToolsMenu",
targets: ["DittoAllToolsMenu"]),
Expand Down Expand Up @@ -125,6 +128,12 @@ let package = Package(
"DittoHealthMetrics"
]
),
.target(
name: "DittoSyncStatusHelper",
dependencies: [
.product(name: "DittoSwift", package: "DittoSwiftPackage")
]
),
.target(
name: "DittoAllToolsMenu",
dependencies: [
Expand All @@ -137,7 +146,8 @@ let package = Package(
"DittoExportData",
"DittoPresenceDegradation",
"DittoHeartbeat",
"DittoPermissionsHealth"
"DittoPermissionsHealth",
"DittoSyncStatusHelper"
]
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// DittoSyncStatusHelper.swift
// DittoSwiftTools
//
// Created by Brian Plattenburg on 11/16/24.
//

import Combine
import DittoSwift

public typealias DittoSyncSubscriptionStatusHandler = (_ result: DittoSyncSubscriptionsStatus) -> Void

/// A status that describes whether a set of `DittoSyncSubscription`s is syncing or idle.
/// This can be combined with an online / offline check to provide an approximation of whether this subscription is up to date
public enum DittoSyncSubscriptionsStatus: String {
case idle
case syncing
}

/**
A helper which provide the sync status of a set of DittoSyncSubscriptions, either idle or syncing.
This tells you if this peer is actively receiving data about this subscription from connected peers or idling
It can be used to provide an approximation of whether this peer is up to date with other connected peers.
It works by creating local store observers for each passed in subscription, then tracking when they fire and comparing against the `idleTimeoutInterval`
*/
public class DittoSubscriptionsStatusHelper {
/// The interval after which a subscription is considered to be idle. Defaults to 1 second.
public var idleTimeoutInterval: TimeInterval = 1

/// The current status for the total set of subscriptions monitored by this helper. This is both `@Published`
/// fired to `handler` via `didSet` when the value changes.
@Published public private(set) var status: DittoSyncSubscriptionsStatus = .idle {
didSet {
guard oldValue != status else { return }
handler?(status)
}
}

private let subscriptions: Set<DittoSyncSubscription>
private let handler: DittoSyncSubscriptionStatusHandler?

private var timer: Timer? = nil
private var observers: [DittoStoreObserver] = []
private var lastUpdated: Date = .distantPast

/**
Creates a new `DittoSyncStatusHelper` for a given set of `DittoSyncSubscription`s
- Parameters:
- ditto: A Ditto instance for which sync status is being checked. Used internally to create `DittoStoreObserver`s tracking each query.
- subscriptions: Which subscriptions to include for this status helper. The aggregate status for all of them will be tracked here, such that it is only considered `idle` if all subscriptions are `idle`.
- handler: An closure called each time the `status` changes. Defaults to `nil`
*/
init(ditto: Ditto, subscriptions: Set<DittoSyncSubscription>, handler: DittoSyncSubscriptionStatusHandler? = nil) throws {
self.subscriptions = subscriptions
self.handler = handler
handler?(.idle)
self.observers = try subscriptions.map { subscription in
try ditto.store.registerObserver(query: subscription.queryString, arguments: subscription.queryArguments, handler: handleObserver)
}
}

/**
Creates a new `DittoSyncStatusHelper` for all of the currently active subscriptions on this Ditto instance *at the time this is created*. It will not update if those subscriptions change
- Parameters:
- ditto: A Ditto instance for which sync status is being checked. Used internally to create `DittoStoreObserver`s tracking each query.
- handler: A closure called each time the `status` changes.
*/
convenience init(ditto: Ditto, handler: DittoSyncSubscriptionStatusHandler?) throws {
try self.init(ditto: ditto, subscriptions: ditto.sync.subscriptions, handler: handler)
}

deinit {
timer?.invalidate()
observers.forEach { observer in
observer.cancel()
}
}

private func handleObserver(_ result: DittoSwift.DittoQueryResult) {
status = .syncing
lastUpdated = Date()
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: idleTimeoutInterval, repeats: false, block: { [weak self] _ in
self?.status = .idle
})
}
}