Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Request Deduplication #24

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EACEC3F529953DCB008242AA"
BuildableName = "GoodNetworking-Sample.app"
BlueprintName = "GoodNetworking-Sample"
ReferencedContainer = "container:GoodNetworking-Sample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EACEC3F529953DCB008242AA"
BuildableName = "GoodNetworking-Sample.app"
BlueprintName = "GoodNetworking-Sample"
ReferencedContainer = "container:GoodNetworking-Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EACEC3F529953DCB008242AA"
BuildableName = "GoodNetworking-Sample.app"
BlueprintName = "GoodNetworking-Sample"
ReferencedContainer = "container:GoodNetworking-Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ final class RequestManager: RequestManagerType {
}

func fetchHero(heroId: Int) -> RequestPublisher<HeroResponse> {
return session.request(endpoint: Endpoint.hero(id: heroId))
.goodify()
return session.execute(endpoint: Endpoint.hero(id: heroId))
.eraseToAnyPublisher()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,18 @@ final class HomeViewModel {
extension HomeViewModel {

func fetchHero() {
di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range))
.map{ HeroFetchingResultState.success($0) }
.catch { Just(HeroFetchingResultState.error($0)) }
.prepend(HeroFetchingResultState.loading)
.eraseToAnyPublisher()
.sink { [weak self] result in self?.heroResult.send(result) }
.store(in: &cancellables)
// di.requestManager.fetchHero(heroId: Int.random(in: Constants.Hero.range))
let heroID = Int.random(in: Constants.Hero.range)
return Publishers.Merge(
di.requestManager.fetchHero(heroId: heroID),
di.requestManager.fetchHero(heroId: heroID)
)
.map{ HeroFetchingResultState.success($0) }
.catch { Just(HeroFetchingResultState.error($0)) }
.prepend(HeroFetchingResultState.loading)
.eraseToAnyPublisher()
.sink { [weak self] result in self?.heroResult.send(result) }
.store(in: &cancellables)
}

func goToAbout() {
Expand Down
22 changes: 22 additions & 0 deletions Sources/GoodNetworking/Executor/ExecutorTask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ExecutorTask.swift
//
//
// Created by Matus Klasovity on 06/08/2024.
//

import Foundation
import Alamofire

final class ExecutorTask<T: Decodable & Sendable> {

var finishDate: Date?
let taskID: String
let task: Task<DataResponse<T, AFError>, Never>

init(taskID: String, task: Task<DataResponse<T, AFError>, Never>) {
self.taskID = taskID
self.task = task
}

}
77 changes: 77 additions & 0 deletions Sources/GoodNetworking/Executor/RequestExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// RequestExecutor.swift
//
//
// Created by Matus Klasovity on 06/08/2024.
//

import Foundation
import Alamofire

actor RequestExecutor {

private let logger: SessionLogger = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GoodLogger použi

if #available(iOS 14, *) {
return OSLogLogger()
} else {
return PrintLogger()
}
}()

private var runningRequestTasks: [String: Any] = [:]

func execute<SuccessType: Decodable>(
_ request: DataRequest,
taskID: String,
deduplicate: Bool,
validResponseCodes: Set<Int>,
emptyResponseCodes: Set<Int>,
emptyResponseMethods: Set<HTTPMethod>
) async -> DataResponse<SuccessType, AFError> {
let randomUUID = UUID().uuidString
return await execute(
request,
taskID: deduplicate ? taskID : randomUUID,
validResponseCodes: validResponseCodes,
emptyResponseCodes: emptyResponseCodes,
emptyResponseMethods: emptyResponseMethods
)
}

private func execute<SuccessType: Decodable & Sendable>(
_ request: DataRequest,
taskID: String,
validResponseCodes: Set<Int>,
emptyResponseCodes: Set<Int>,
emptyResponseMethods: Set<HTTPMethod>
) async -> DataResponse<SuccessType, AFError> {
if let runningTask = runningRequestTasks[taskID] {
let executorTask = runningTask as! ExecutorTask<SuccessType>
logger.log(level: .info, message: "🚀 taskID: \(taskID) Cached value used")
return await executorTask.task.value
} else {
let requestTask = Task {
return await request.goodifyAsync(
validResponseCodes: validResponseCodes,
emptyResponseCodes: emptyResponseCodes,
emptyResponseMethods: emptyResponseMethods
) as DataResponse<SuccessType, AFError>
}

logger.log(level: .info, message: "🚀 taskID: \(taskID): Task created")
let executorTask: ExecutorTask = ExecutorTask(
taskID: taskID,
task: requestTask
)

runningRequestTasks[taskID] = executorTask

let result = await requestTask.value

logger.log(level: .info, message: "🚀 taskID: \(taskID): Task finished successfully")
return result
}
}

}

43 changes: 43 additions & 0 deletions Sources/GoodNetworking/Extensions/DataRequestExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,49 @@ public extension DataRequest {
.value()
}

func goodifyAsync<T: Decodable & Sendable>(
type: T.Type = T.self,
validResponseCodes: Set<Int> = Set(200..<299),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods
) async throws -> T {
do {
return try await self
.validate(statusCode: validResponseCodes)
.serializingDecodable(
T.self,
automaticallyCancelling: true,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyResponseMethods
)
.value
} catch let afError as AFError {
// potentionally the place for alamofire non fatal crashes logger
throw afError
} catch {
throw error
}
}


func goodifyAsync<T: Decodable & Sendable>(
type: T.Type = T.self,
validResponseCodes: Set<Int> = Set(200..<299),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods
) async -> DataResponse<T, AFError> {
await self
.validate(statusCode: validResponseCodes)
.serializingDecodable(
T.self,
automaticallyCancelling: true,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyResponseMethods
)
.response
}


}

// MARK: - Private
Expand Down
47 changes: 47 additions & 0 deletions Sources/GoodNetworking/Extensions/FutureExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// FutureExtensions.swift
//
//
// Created by Matus Klasovity on 06/08/2024.
//

import Foundation
import Combine

extension Future where Failure == Error {

static func create(asyncThrowableFunc: @Sendable @escaping () async throws -> Output) -> Self {
Self.init { promise in
nonisolated(unsafe) let promise = promise
Task {
do {
let result = try await asyncThrowableFunc()
await MainActor.run {
promise(.success(result))
}
} catch {
await MainActor.run {
promise(.failure(error))
}
}
}
}
}

}

extension Future where Failure == Never {

static func create(asyncFunc: @Sendable @escaping () async -> Output) -> Self {
Self.init { promise in
nonisolated(unsafe) let promise = promise
Task {
let result = await asyncFunc()
await MainActor.run {
promise(.success(result))
}
}
}
}

}
Loading