-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
State driven side effects core (#26)
* implemented job state * implemented side effects prototype * refactoring * refactoring * side effect for flag * non-nil Effect * refactoring * refactoring * refactoring * added effects for state store * added tests for bool state-driven side effects * added equatable state-driven side effects tests * refactoring * added tests for state - driven effects * minor fix * refactoring * fixed tests * added effects collection tests * renamed tests * refactor * renamed side effects * renamed tests * side effects execution order fix * moved some of the effects execution logic to operator * added effect state tests * minor refactoring * fixed observer bug * renamed * minor fix * observer refactoring * fixed bug * refactored * refactoring * minor test fix * effect failure fixes
- Loading branch information
1 parent
1cd2e87
commit 9b3e0e1
Showing
15 changed files
with
1,637 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Sergey Kazakov on 13/04/2024. | ||
// | ||
|
||
import Foundation | ||
|
||
struct Effect { | ||
typealias Operation = () -> Void | ||
|
||
private let perform: Operation? | ||
|
||
var operation: Operation { | ||
perform ?? { } | ||
} | ||
|
||
var canBeExecuted: Bool { | ||
perform != nil | ||
} | ||
|
||
init(_ operation: @escaping () -> Void) { | ||
perform = operation | ||
} | ||
|
||
private init(operation: Operation?) { | ||
perform = operation | ||
} | ||
|
||
|
||
@available(iOS 13.0, *) | ||
init(operation: @escaping () async -> Void) { | ||
perform = { | ||
Task { | ||
await operation() | ||
} | ||
} | ||
} | ||
|
||
static let skip: Effect = Effect(operation: nil) | ||
} | ||
|
||
extension Effect { | ||
struct State: Codable, Equatable, Hashable { | ||
typealias ID = UUID | ||
|
||
private(set) var id = ID() | ||
private var state = InternalState() | ||
|
||
var status: Status { | ||
state.status | ||
} | ||
|
||
var isInProgress: Bool { | ||
status.isInProgress | ||
} | ||
|
||
var isSuccess: Bool { | ||
status.isSuccess | ||
} | ||
|
||
var isCancelled: Bool { | ||
status.isCancelled | ||
} | ||
|
||
var isIdle: Bool { | ||
status.isIdle | ||
} | ||
|
||
var isFailed: Bool { | ||
status.isFailed | ||
} | ||
|
||
var error: Error? { | ||
state.error | ||
} | ||
|
||
var currentAttempt: Int? { | ||
state.currentAttempt?.attempt | ||
} | ||
|
||
var delay: TimeInterval? { | ||
state.currentAttempt?.delay | ||
} | ||
} | ||
} | ||
|
||
extension Effect.State { | ||
static func running(maxAttempts: Int = 1, | ||
delay: TimeInterval = .zero) -> Effect.State { | ||
|
||
var effect = Effect.State() | ||
effect.run(maxAttempts: maxAttempts, delay: delay) | ||
return effect | ||
} | ||
|
||
static func idle() -> Effect.State { | ||
Effect.State() | ||
} | ||
} | ||
|
||
extension Effect.State { | ||
|
||
enum Status: Int { | ||
case none | ||
case inProgress | ||
case success | ||
case failure | ||
case cancelled | ||
} | ||
} | ||
|
||
extension Effect.State.Status { | ||
var isInProgress: Bool { | ||
self == .inProgress | ||
} | ||
|
||
var isSuccess: Bool { | ||
self == .success | ||
} | ||
|
||
var isCancelled: Bool { | ||
self == .cancelled | ||
} | ||
|
||
var isIdle: Bool { | ||
self == .none | ||
} | ||
|
||
var isFailed: Bool { | ||
self == .failure | ||
} | ||
} | ||
|
||
extension Effect.State { | ||
|
||
mutating func run(maxAttempts: Int = 1, | ||
delay: TimeInterval = .zero) { | ||
|
||
state.run(maxAttempts: maxAttempts, delay: delay) | ||
} | ||
|
||
mutating func restart(maxAttempts: Int = 1, | ||
delay: TimeInterval = .zero) { | ||
|
||
state.restart(maxAttempts: maxAttempts, delay: delay) | ||
} | ||
|
||
mutating func retryOrFailWith(_ error: Error?) { | ||
state.retryOrFailWith(error) | ||
} | ||
|
||
mutating func cancel() { | ||
state.cancel() | ||
} | ||
|
||
mutating func succeed() { | ||
state.succeed() | ||
} | ||
|
||
mutating func reset() { | ||
state.reset() | ||
} | ||
|
||
mutating func fail(_ error: Error?) { | ||
state.fail(error) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Sergey Kazakov on 13/04/2024. | ||
// | ||
|
||
import Dispatch | ||
import Foundation | ||
|
||
final class EffectOperator { | ||
private(set) var executing: [Effect.State: Weak<DispatchWorkItem>] = [:] | ||
private(set) var isSynced = true | ||
|
||
func run(_ inProgress: Bool, | ||
on queue: DispatchQueue, | ||
create: (Effect.State) -> Effect) { | ||
|
||
let effect: Effect.State = inProgress ? .running() : .idle() | ||
run(effect, on: queue, create: create) | ||
} | ||
|
||
func run(_ effect: Effect.State, | ||
on queue: DispatchQueue, | ||
create: (Effect.State) -> Effect) { | ||
|
||
run([effect], on: queue, create: create) | ||
} | ||
|
||
func run<Effects>(_ effects: Effects, | ||
on queue: DispatchQueue, | ||
create: (Effect.State) -> Effect) | ||
|
||
where Effects: Collection & Hashable, Effects.Element == Effect.State { | ||
|
||
let effectsInProgress = effects.filter { $0.isInProgress } | ||
let expectedToBeExecuting = Set(effectsInProgress) | ||
|
||
executing.keys | ||
.filter { !expectedToBeExecuting.contains($0) } | ||
.forEach { | ||
executing[$0]?.object?.cancel() | ||
executing[$0] = nil | ||
} | ||
|
||
effectsInProgress | ||
.filter { !executing.keys.contains($0) } | ||
.map { state in (state, create(state)) } | ||
.filter { _, effect in effect.canBeExecuted } | ||
.forEach { state, effect in | ||
let workItem = DispatchWorkItem(block: effect.operation) | ||
executing[state] = Weak(workItem) | ||
queue.asyncAfter(delay: state.delay, execute: workItem) | ||
} | ||
|
||
isSynced = expectedToBeExecuting.count == executing.count | ||
} | ||
} |
Oops, something went wrong.