Skip to content

Commit 8456987

Browse files
Merge pull request #7 from novasamatech/feature/combining-service
Operation combining service
2 parents e607713 + f7db516 commit 8456987

File tree

6 files changed

+251
-7
lines changed

6 files changed

+251
-7
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
3+
extension CompoundOperationWrapper {
4+
public func addDependency(operations: [Operation]) {
5+
for nextOperation in allOperations {
6+
for prevOperation in operations {
7+
nextOperation.addDependency(prevOperation)
8+
}
9+
}
10+
}
11+
12+
public func addDependency(wrapper: CompoundOperationWrapper<some Any>) {
13+
addDependency(operations: wrapper.allOperations)
14+
}
15+
16+
public func insertingHead(operations: [Operation]) -> CompoundOperationWrapper {
17+
.init(targetOperation: targetOperation, dependencies: operations + dependencies)
18+
}
19+
20+
public func insertingTail<T>(operation: BaseOperation<T>) -> CompoundOperationWrapper<T> {
21+
.init(targetOperation: operation, dependencies: allOperations)
22+
}
23+
}
24+
25+
extension CompoundOperationWrapper {
26+
public static func createWithError(_ error: Error) -> CompoundOperationWrapper<ResultType> {
27+
let operation = BaseOperation<ResultType>()
28+
operation.result = .failure(error)
29+
return CompoundOperationWrapper(targetOperation: operation)
30+
}
31+
32+
public static func createWithResult(_ result: ResultType) -> CompoundOperationWrapper<ResultType> {
33+
let operation = BaseOperation<ResultType>()
34+
operation.result = .success(result)
35+
return CompoundOperationWrapper(targetOperation: operation)
36+
}
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
public protocol Longrunable {
4+
associatedtype ResultType
5+
6+
func start(with completionClosure: @escaping (Result<ResultType, Error>) -> Void)
7+
func cancel()
8+
}
9+
10+
public final class AnyLongrun<T>: Longrunable {
11+
public typealias ResultType = T
12+
13+
private let privateStart: (@escaping (Result<ResultType, Error>) -> Void) -> Void
14+
private let privateCancel: () -> Void
15+
16+
public init<U: Longrunable>(longrun: U) where U.ResultType == ResultType {
17+
privateStart = longrun.start
18+
privateCancel = longrun.cancel
19+
}
20+
21+
public func start(with completionClosure: @escaping (Result<T, Error>) -> Void) {
22+
privateStart(completionClosure)
23+
}
24+
25+
public func cancel() {
26+
privateCancel()
27+
}
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
public final class LongrunOperation<T>: BaseOperation<T> {
4+
let longrun: AnyLongrun<T>
5+
6+
public init(longrun: AnyLongrun<T>) {
7+
self.longrun = longrun
8+
}
9+
10+
public override func performAsync(
11+
_ callback: @escaping (Result<T, Error>) -> Void
12+
) throws {
13+
longrun.start(with: callback)
14+
}
15+
16+
public override func cancel() {
17+
longrun.cancel()
18+
19+
super.cancel()
20+
}
21+
}

Operation-iOS/Classes/Operations/Network/NetworkDataError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public enum NetworkResponseError: Error {
4545

4646
static func createFrom(statusCode: Int) -> NetworkResponseError? {
4747
switch statusCode {
48-
case 200:
48+
case 200...299:
4949
return nil
5050
case 400:
5151
return NetworkResponseError.invalidParameters

Operation-iOS/Classes/Operations/Network/NetworkResultFactory.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ public final class AnyNetworkResultFactory<T>: NetworkResultFactoryProtocol {
112112
return .failure(NetworkBaseError.unexpectedEmptyData)
113113
}
114114

115-
do {
116-
let value = try processingBlock(documentData)
117-
return .success(value)
118-
} catch {
119-
return .failure(error)
120-
}
115+
return Result { try processingBlock(documentData) }
121116
}
122117
}
123118

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import Foundation
2+
3+
public enum OperationCombiningServiceError: Error {
4+
case alreadyRunningOrFinished
5+
case noResult
6+
}
7+
8+
public final class OperationCombiningService<T>: Longrunable, @unchecked Sendable {
9+
enum State {
10+
case waiting
11+
case running
12+
case finished
13+
}
14+
15+
public typealias ResultType = [T]
16+
17+
let operationsClosure: () throws -> [CompoundOperationWrapper<T>]
18+
let operationManager: OperationManagerProtocol
19+
let operationsPerBatch: Int
20+
21+
private(set) var state: State = .waiting
22+
23+
private var wrappers: [CompoundOperationWrapper<T>]?
24+
25+
public init(
26+
operationManager: OperationManagerProtocol,
27+
operationsPerBatch: Int = 0,
28+
operationsClosure: @escaping () throws -> [CompoundOperationWrapper<T>]
29+
) {
30+
self.operationManager = operationManager
31+
self.operationsClosure = operationsClosure
32+
self.operationsPerBatch = operationsPerBatch
33+
}
34+
35+
public func start(with completionClosure: @escaping (Result<ResultType, Error>) -> Void) {
36+
guard state == .waiting else {
37+
completionClosure(.failure(OperationCombiningServiceError.alreadyRunningOrFinished))
38+
return
39+
}
40+
41+
state = .waiting
42+
43+
do {
44+
let wrappers = try operationsClosure()
45+
46+
if operationsPerBatch > 0, wrappers.count > operationsPerBatch {
47+
for index in operationsPerBatch ..< wrappers.count {
48+
let prevBatchIndex = index / operationsPerBatch - 1
49+
50+
let prevStart = prevBatchIndex * operationsPerBatch
51+
let prevEnd = (prevBatchIndex + 1) * operationsPerBatch
52+
53+
for prevIndex in prevStart ..< prevEnd {
54+
wrappers[index].addDependency(wrapper: wrappers[prevIndex])
55+
}
56+
}
57+
}
58+
59+
let mapOperation = ClosureOperation<ResultType> {
60+
try wrappers.map { try $0.targetOperation.extractNoCancellableResultData() }
61+
}
62+
63+
// TODO: Need to fix Sendable, temporary solution
64+
nonisolated(unsafe) let completionClosure = completionClosure
65+
66+
mapOperation.completionBlock = { [weak self] in
67+
self?.state = .finished
68+
self?.wrappers = nil
69+
70+
let result = Result { try mapOperation.extractNoCancellableResultData() }
71+
completionClosure(result)
72+
}
73+
74+
let dependencies = wrappers.flatMap(\.allOperations)
75+
dependencies.forEach { mapOperation.addDependency($0) }
76+
77+
operationManager.enqueue(operations: dependencies + [mapOperation], in: .transient)
78+
79+
} catch {
80+
completionClosure(.failure(error))
81+
}
82+
}
83+
84+
public func cancel() {
85+
if state == .running {
86+
wrappers?.forEach { $0.cancel() }
87+
wrappers = nil
88+
}
89+
90+
state = .finished
91+
}
92+
}
93+
94+
extension OperationCombiningService {
95+
public func longrunOperation() -> LongrunOperation<[T]> {
96+
LongrunOperation(longrun: AnyLongrun(longrun: self))
97+
}
98+
99+
public static func compoundWrapper(
100+
operationManager: OperationManagerProtocol,
101+
wrapperClosure: @escaping () throws -> CompoundOperationWrapper<T>?
102+
) -> CompoundOperationWrapper<T?> {
103+
let loadingOperation: BaseOperation<[T]> = OperationCombiningService(operationManager: operationManager) {
104+
if let wrapper = try wrapperClosure() {
105+
[wrapper]
106+
} else {
107+
[]
108+
}
109+
}.longrunOperation()
110+
111+
let mappingOperation = ClosureOperation<T?> {
112+
try loadingOperation.extractNoCancellableResultData().first
113+
}
114+
115+
mappingOperation.addDependency(loadingOperation)
116+
117+
return .init(targetOperation: mappingOperation, dependencies: [loadingOperation])
118+
}
119+
120+
public static func compoundOptionalWrapper(
121+
operationManager: OperationManagerProtocol,
122+
wrapperClosure: @escaping () throws -> CompoundOperationWrapper<T?>?
123+
) -> CompoundOperationWrapper<T?> {
124+
let loadingOperation: BaseOperation<[T?]> = OperationCombiningService<T?>(operationManager: operationManager) {
125+
if let wrapper = try wrapperClosure() {
126+
[wrapper]
127+
} else {
128+
[]
129+
}
130+
}.longrunOperation()
131+
132+
let mappingOperation = ClosureOperation<T?> {
133+
let results = try loadingOperation.extractNoCancellableResultData()
134+
return results.first.flatMap { $0 }
135+
}
136+
137+
mappingOperation.addDependency(loadingOperation)
138+
139+
return .init(targetOperation: mappingOperation, dependencies: [loadingOperation])
140+
}
141+
142+
public static func compoundNonOptionalWrapper(
143+
operationManager: OperationManagerProtocol,
144+
wrapperClosure: @escaping () throws -> CompoundOperationWrapper<T>
145+
) -> CompoundOperationWrapper<T> {
146+
let loadingOperation: BaseOperation<[T]> = OperationCombiningService<T>(operationManager: operationManager) {
147+
let wrapper = try wrapperClosure()
148+
return [wrapper]
149+
}.longrunOperation()
150+
151+
let mappingOperation = ClosureOperation<T> {
152+
guard let result = try loadingOperation.extractNoCancellableResultData().first else {
153+
throw OperationCombiningServiceError.noResult
154+
}
155+
156+
return result
157+
}
158+
159+
mappingOperation.addDependency(loadingOperation)
160+
161+
return .init(targetOperation: mappingOperation, dependencies: [loadingOperation])
162+
}
163+
}

0 commit comments

Comments
 (0)