Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions Sources/ScreenStatetKit/Actions/ActionLocker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,57 @@

import Foundation

public actor ActionLocker {

public struct ActionLocker {

/// Use this when the locker is confined to a single actor or execution context.
/// No additional isolation is required as long as it is not accessed concurrently.
public static var nonIsolated: NonIsolatedActionLocker { .init() }

/// Use this when the locker is shared across multiple actors or concurrent contexts.
/// This variant provides the necessary isolation to ensure thread safety.
public static var isolated: IsolatedActionLocker { .init() }
}

//MARK: - Isolated
public actor IsolatedActionLocker {

let locker: NonIsolatedActionLocker

internal init() {
locker = .init()
}

public func lock(_ action: ActionLockable) throws {
try locker.lock(action)
}

public func unlock(_ action: ActionLockable) {
locker.unlock(action)
}

public func canExecute(_ action: ActionLockable) -> Bool {
locker.canExecute(action)
}

public func free() {
locker.free()
}
}

//MARK: - Nonisolated
public final class NonIsolatedActionLocker {

private var actions: [AnyHashable: Bool]

public init() {
internal init() {
actions = .init()
}

public func lock(_ action: ActionLockable) throws {
let isRunning = actions[action.lockKey] ?? false
guard !isRunning else {
throw Errors.actionIsRunning
throw ActionLocker.Errors.actionIsRunning
}
actions.updateValue(true, forKey: action.lockKey)
}
Expand Down
43 changes: 29 additions & 14 deletions Sources/ScreenStatetKit/Helpers/CancelBag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,55 @@ import Foundation

public actor CancelBag {

private var cancellers: [String:Canceller]
private let storage: CancelBagStorage

public init() {
cancellers = .init()
storage = .init()
}

public func cancelAll() {
storage.cancelAll()
}

public func cancel(forIdentifier identifier: String) {
storage.cancel(forIdentifier: identifier)
}

private func insert(_ canceller: Canceller) {
storage.insert(canceller: canceller)
}

nonisolated fileprivate func append(canceller: Canceller) {
Task(priority: .high) {
await insert(canceller)
}
}
}

private final class CancelBagStorage {

private var cancellers: [String: Canceller] = [:]

func cancelAll() {
let runningTasks = cancellers.values.filter({ !$0.isCancelled })
runningTasks.forEach{ $0.cancel() }
cancellers.removeAll()
}

public func cancel(forIdentifier identifier: String) {
func cancel(forIdentifier identifier: String) {
guard let task = cancellers[identifier] else { return }
task.cancel()
cancellers.removeValue(forKey: identifier)
}

nonisolated public func cancelAllInTask() {
Task(priority: .high) {
await cancelAll()
}
}

private func store(_ canceller: Canceller) {
func insert(canceller: Canceller) {
cancel(forIdentifier: canceller.id)
guard !canceller.isCancelled else { return }
cancellers.updateValue(canceller, forKey: canceller.id)
}

nonisolated fileprivate func append(canceller: Canceller) {
Task(priority: .high) {
await store(canceller)
}
deinit {
cancelAll()
}
}

Expand Down
12 changes: 5 additions & 7 deletions Sources/ScreenStatetKit/States/StateUpdatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,18 @@ import SwiftUI
@MainActor
public protocol StateUpdatable {

func updateState( _ updateBlock: @MainActor (_ state: Self) -> Void,
withAnimation animation: Animation?,
disablesAnimations: Bool)
func updateState(withAnimation animation: Animation?,
_ updateBlock: @MainActor (_ state: Self) -> Void)
}


extension StateUpdatable {

public func updateState( _ updateBlock: @MainActor (_ state: Self) -> Void,
withAnimation animation: Animation? = .none,
disablesAnimations: Bool = false) {
public func updateState(withAnimation animation: Animation? = .smooth,
_ updateBlock: @MainActor (_ state: Self) -> Void) {
var transaction = Transaction()
transaction.animation = animation
transaction.disablesAnimations = disablesAnimations
transaction.disablesAnimations = animation == .none
withTransaction(transaction) {
updateBlock(self)
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/ScreenStatetKitTests/Actions/ActionLockerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ extension ActionLockerTests {
case loadMore
}

private func makeSUT() -> ActionLocker {
ActionLocker()
private func makeSUT() -> IsolatedActionLocker {
ActionLocker.isolated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ScreenStateKit
extension StoreStateIntegrationTests {
actor TestLoadmoreStore: ScreenActionStore {
private var state: TestLoadmoreState?
private let actionLocker = ActionLocker()
private let actionLocker = ActionLocker.nonIsolated

func binding(state: TestLoadmoreState) {
self.state = state
Expand All @@ -22,7 +22,7 @@ extension StoreStateIntegrationTests {
}

func isolatedReceive(action: Action) async {
guard await actionLocker.canExecute(action) else { return }
guard actionLocker.canExecute(action) else { return }
await state?.loadingStarted(action: action)

switch action {
Expand All @@ -43,7 +43,7 @@ extension StoreStateIntegrationTests {
await state?.ternimateLoadmoreView()
}

await actionLocker.unlock(action)
actionLocker.unlock(action)
await state?.loadingFinished(action: action)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension StoreStateIntegrationTests {

actor TestStore: ScreenActionStore {
private var state: TestScreenState?
private let actionLocker = ActionLocker()
private let actionLocker = ActionLocker.nonIsolated
private(set) var fetchCount = 0

func binding(state: TestScreenState) {
Expand All @@ -25,7 +25,7 @@ extension StoreStateIntegrationTests {
}

func isolatedReceive(action: Action) async {
guard await actionLocker.canExecute(action) else { return }
guard actionLocker.canExecute(action) else { return }
await state?.loadingStarted(action: action)

do {
Expand All @@ -52,7 +52,7 @@ extension StoreStateIntegrationTests {
await state?.showError(RMDisplayableError(message: error.localizedDescription))
}

await actionLocker.unlock(action)
actionLocker.unlock(action)
await state?.loadingFinished(action: action)
}

Expand Down