fix: Prevent redundant timer creation in offline sync state machine
- Guard against multiple concurrent completion timers in sync state handling - Only trigger completion timer when transitioning from actual syncing state - Remove debug logging that impacted performance during scroll operations This resolves scroll performance issues introduced by excessive timer creation in the offline bookmark synchronization workflow.
This commit is contained in:
parent
ffb41347af
commit
692f34d2ce
@ -8,76 +8,16 @@ class OfflineBookmarksViewModel {
|
|||||||
|
|
||||||
private let syncUseCase: POfflineBookmarkSyncUseCase
|
private let syncUseCase: POfflineBookmarkSyncUseCase
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var successTimer: Timer?
|
private let successDelaySubject = PassthroughSubject<Int, Never>()
|
||||||
|
private var completionTimerActive = false
|
||||||
|
|
||||||
init(syncUseCase: POfflineBookmarkSyncUseCase = OfflineBookmarkSyncUseCase()) {
|
init(syncUseCase: POfflineBookmarkSyncUseCase = OfflineBookmarkSyncUseCase()) {
|
||||||
self.syncUseCase = syncUseCase
|
self.syncUseCase = syncUseCase
|
||||||
setupBindings()
|
setupBindings()
|
||||||
updateState()
|
refreshState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupBindings() {
|
// MARK: - Public Methods
|
||||||
// Observe sync state changes
|
|
||||||
syncUseCase.isSyncing
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] isSyncing in
|
|
||||||
self?.handleSyncStateChange(isSyncing: isSyncing)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
// Observe sync status changes
|
|
||||||
syncUseCase.syncStatus
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] status in
|
|
||||||
self?.handleSyncStatusChange(status: status)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
// Update count on app lifecycle events
|
|
||||||
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.updateState()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.updateState()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateState() {
|
|
||||||
let count = syncUseCase.getOfflineBookmarksCount()
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case .idle:
|
|
||||||
if count > 0 {
|
|
||||||
state = .pending(count: count)
|
|
||||||
}
|
|
||||||
case .pending:
|
|
||||||
if count > 0 {
|
|
||||||
state = .pending(count: count)
|
|
||||||
} else {
|
|
||||||
state = .idle
|
|
||||||
}
|
|
||||||
case .syncing:
|
|
||||||
// Keep syncing state, will be updated by handleSyncStateChange
|
|
||||||
break
|
|
||||||
case .success:
|
|
||||||
// Success state is temporary, handled by timer
|
|
||||||
break
|
|
||||||
case .error:
|
|
||||||
// Update count even in error state
|
|
||||||
if count > 0 {
|
|
||||||
state = .pending(count: count)
|
|
||||||
} else {
|
|
||||||
state = .idle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncOfflineBookmarks() async {
|
func syncOfflineBookmarks() async {
|
||||||
guard case .pending(let count) = state else { return }
|
guard case .pending(let count) = state else { return }
|
||||||
@ -86,46 +26,133 @@ class OfflineBookmarksViewModel {
|
|||||||
await syncUseCase.syncOfflineBookmarks()
|
await syncUseCase.syncOfflineBookmarks()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSyncStateChange(isSyncing: Bool) {
|
func refreshState() {
|
||||||
if isSyncing {
|
let currentCount = syncUseCase.getOfflineBookmarksCount()
|
||||||
// If we're not already in syncing state, transition to it
|
updateStateWithCount(currentCount)
|
||||||
if case .pending(let count) = state {
|
}
|
||||||
state = .syncing(count: count, status: nil)
|
|
||||||
|
// MARK: - Private Setup
|
||||||
|
|
||||||
|
private func setupBindings() {
|
||||||
|
setupSyncBindings()
|
||||||
|
setupAppLifecycleBindings()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSyncBindings() {
|
||||||
|
syncUseCase.isSyncing
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isSyncing in
|
||||||
|
self?.handleSyncingStateChange(isSyncing)
|
||||||
}
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
syncUseCase.syncStatus
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] status in
|
||||||
|
self?.handleSyncStatusUpdate(status)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Auto-reset success state after 2 seconds
|
||||||
|
successDelaySubject
|
||||||
|
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.state = .idle
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupAppLifecycleBindings() {
|
||||||
|
let foregroundPublisher = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
||||||
|
let activePublisher = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
||||||
|
|
||||||
|
Publishers.Merge(foregroundPublisher, activePublisher)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.refreshState()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State Management
|
||||||
|
|
||||||
|
private func updateStateWithCount(_ count: Int) {
|
||||||
|
switch state {
|
||||||
|
case .idle:
|
||||||
|
if count > 0 {
|
||||||
|
state = .pending(count: count)
|
||||||
|
}
|
||||||
|
case .pending:
|
||||||
|
state = count > 0 ? .pending(count: count) : .idle
|
||||||
|
case .syncing:
|
||||||
|
// Keep syncing state - will be updated by sync handlers
|
||||||
|
break
|
||||||
|
case .success:
|
||||||
|
// Success state is temporary - handled by timer
|
||||||
|
break
|
||||||
|
case .error:
|
||||||
|
state = count > 0 ? .pending(count: count) : .idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sync Event Handlers
|
||||||
|
|
||||||
|
private func handleSyncingStateChange(_ isSyncing: Bool) {
|
||||||
|
if isSyncing {
|
||||||
|
transitionToSyncingIfPending()
|
||||||
} else {
|
} else {
|
||||||
// Sync completed
|
// Only handle completion if we were actually syncing
|
||||||
Task { @MainActor in
|
if case .syncing = state {
|
||||||
// Small delay to ensure count is updated
|
handleSyncCompletion()
|
||||||
try await Task.sleep(nanoseconds: 500_000_000)
|
|
||||||
|
|
||||||
let currentCount = syncUseCase.getOfflineBookmarksCount()
|
|
||||||
|
|
||||||
if case .syncing(let originalCount, _) = state {
|
|
||||||
if currentCount == 0 {
|
|
||||||
// Success - all bookmarks synced
|
|
||||||
state = .success(syncedCount: originalCount)
|
|
||||||
|
|
||||||
// Auto-hide success message after 2 seconds
|
|
||||||
successTimer?.invalidate()
|
|
||||||
successTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
|
|
||||||
self?.state = .idle
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Some bookmarks remain
|
|
||||||
state = .pending(count: currentCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSyncStatusChange(status: String?) {
|
private func transitionToSyncingIfPending() {
|
||||||
|
if case .pending(let count) = state {
|
||||||
|
state = .syncing(count: count, status: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSyncCompletion() {
|
||||||
|
guard !completionTimerActive else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
completionTimerActive = true
|
||||||
|
|
||||||
|
// wait for 0.5 seconds
|
||||||
|
Timer.publish(every: 0.5, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.first()
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.completionTimerActive = false
|
||||||
|
|
||||||
|
guard case .syncing(let originalCount, _) = self.state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainingCount = self.syncUseCase.getOfflineBookmarksCount()
|
||||||
|
|
||||||
|
if remainingCount == 0 {
|
||||||
|
self.state = .success(syncedCount: originalCount)
|
||||||
|
self.successDelaySubject.send(originalCount)
|
||||||
|
} else {
|
||||||
|
self.state = .pending(count: remainingCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSyncStatusUpdate(_ status: String?) {
|
||||||
if case .syncing(let count, _) = state {
|
if case .syncing(let count, _) = state {
|
||||||
state = .syncing(count: count, status: status)
|
state = .syncing(count: count, status: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
successTimer?.invalidate()
|
cancellables.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user