Skip to content

Commit

Permalink
State driven side effects core (#26)
Browse files Browse the repository at this point in the history
* 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
KazaiMazai committed Apr 18, 2024
1 parent 1cd2e87 commit 9b3e0e1
Show file tree
Hide file tree
Showing 15 changed files with 1,637 additions and 50 deletions.
170 changes: 170 additions & 0 deletions Sources/Puredux/SideEffects/Effect.swift
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)
}
}

58 changes: 58 additions & 0 deletions Sources/Puredux/SideEffects/EffectOperator.swift
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
}
}
Loading

0 comments on commit 9b3e0e1

Please sign in to comment.