Skip to content

Commit

Permalink
SPT-1998 add check cancellation to nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
mrandrewsmith committed May 2, 2024
1 parent ded2986 commit 79bc0d6
Show file tree
Hide file tree
Showing 73 changed files with 2,416 additions and 376 deletions.
4 changes: 3 additions & 1 deletion Example/Example/Features/GroupFeature/GroupPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ private extension GroupPresenter {
await input?.hideLoader()
await input?.update(with: viewModel)
} catch {
await input?.show(error: error)
if !(error is CancellationError) {
await input?.show(error: error)
}
}
}
}
Expand Down
46 changes: 33 additions & 13 deletions Example/Example/Features/GroupFeature/GroupViewModelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,32 @@ actor GroupViewModelProvider: GroupViewModelProviderProtocol {
func provide() async throws -> GroupViewModel {
cancelAllTasks()

/// Создаем таски, которые при ошибке останавливают все сохраненные таски.

let headerTask = storedTask { await $0.header() }
let bodyTask = storedTask { await $0.body() }
let footerTask = storedTask { await $0.footer() }

async let header = headerTask.value
async let body = bodyTask.value
async let footer = footerTask.value
/// Ждем результаты.
/// Так как CancellationError не в приоритете, игнорируем ее на этом этапе.
/// По всем остальным ошибка словим Exception.

let header = try await resultWithCheckedError(from: headerTask)
let body = try await resultWithCheckedError(from: bodyTask)
let footer = try await resultWithCheckedError(from: footerTask)

return try await GroupViewModel(
headerTitle: header.text,
headerImage: header.image,
bodyTitle: body.text,
bodyImage: body.image,
footerTitle: footer.text,
footerImage: footer.image
/// Собираем модель.
/// На этом этапе мы можем словить Exception только c CancellationError.
/// Необходимо обработать ее на уровне выше.
/// Кейс когда словили CancellationError - все остановленные таски отработали без ошибок.

return try GroupViewModel(
headerTitle: header.get().text,
headerImage: header.get().image,
bodyTitle: body.get().text,
bodyImage: body.get().image,
footerTitle: footer.get().text,
footerImage: footer.get().image
)
}
}
Expand All @@ -56,19 +67,28 @@ actor GroupViewModelProvider: GroupViewModelProviderProtocol {

private extension GroupViewModelProvider {

func storedTask<T>(_ nodeResult: @escaping (GroupServiceProtocol) async -> NodeResult<T>) -> Task<T, Error> {
func storedTask<T>(_ nodeResult: @escaping (GroupServiceProtocol) async -> NodeResult<T>) -> Task<NodeResult<T>, Never> {
let task = Task {
try await nodeResult(groupService)
await nodeResult(groupService)
.mapError {
cancelAllTasks()
return $0
}
.get()
}
tasks.append(task)
return task
}

func resultWithCheckedError<T>(from task: Task<NodeResult<T>, Never>) async throws -> NodeResult<T> {
let value = await task.value

if let error = value.error, !(error is CancellationError) {
throw error
}

return value
}

func cancelAllTasks() {
tasks.forEach { $0.cancel() }
tasks.removeAll()
Expand Down
29 changes: 15 additions & 14 deletions NodeKit/NodeKit/CacheNode/ETag/UrlETagReaderNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,24 @@ open class UrlETagReaderNode: AsyncNode {
_ data: TransportUrlRequest,
logContext: LoggingContextProtocol
) async -> NodeResult<Json> {
guard
let key = data.url.withOrderedQuery(),
let tag = UserDefaults.etagStorage?.value(forKey: key) as? String
else {
return await next.process(data, logContext: logContext)
}
await .withCheckedCancellation {
guard
let key = data.url.withOrderedQuery(),
let tag = UserDefaults.etagStorage?.value(forKey: key) as? String
else {
return await next.process(data, logContext: logContext)
}

var headers = data.headers
headers[self.etagHeaderKey] = tag
var headers = data.headers
headers[self.etagHeaderKey] = tag

let params = TransportUrlParameters(method: data.method,
url: data.url,
headers: headers)
let params = TransportUrlParameters(method: data.method,
url: data.url,
headers: headers)

let newData = TransportUrlRequest(with: params, raw: data.raw)
let newData = TransportUrlRequest(with: params, raw: data.raw)

return await next.process(newData, logContext: logContext)
return await next.process(newData, logContext: logContext)
}
}

}
18 changes: 10 additions & 8 deletions NodeKit/NodeKit/CacheNode/ETag/UrlETagSaverNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ open class UrlETagSaverNode: AsyncNode {
_ data: UrlProcessedResponse,
logContext: LoggingContextProtocol
) async -> NodeResult<Void> {
guard let tag = data.response.allHeaderFields[self.eTagHeaderKey] as? String,
let url = data.request.url,
let urlAsKey = url.withOrderedQuery()
else {
return await next?.process(data, logContext: logContext) ?? .success(())
}
await .withCheckedCancellation {
guard let tag = data.response.allHeaderFields[self.eTagHeaderKey] as? String,
let url = data.request.url,
let urlAsKey = url.withOrderedQuery()
else {
return await next?.process(data, logContext: logContext) ?? .success(())
}

UserDefaults.etagStorage?.set(tag, forKey: urlAsKey)
UserDefaults.etagStorage?.set(tag, forKey: urlAsKey)

return await next?.process(data, logContext: logContext) ?? .success(())
return await next?.process(data, logContext: logContext) ?? .success(())
}
}
}

Expand Down
20 changes: 11 additions & 9 deletions NodeKit/NodeKit/CacheNode/IfServerFailsFromCacheNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ open class IfConnectionFailedFromCacheNode: AsyncNode {
_ data: URLRequest,
logContext: LoggingContextProtocol
) async -> NodeResult<Json> {
return await next.process(data, logContext: logContext)
.asyncFlatMapError { error in
let request = UrlNetworkRequest(urlRequest: data)
if error is BaseTechnicalError {
await logContext.add(makeBaseTechinalLog(with: error))
return await cacheReaderNode.process(request, logContext: logContext)
await .withCheckedCancellation {
await next.process(data, logContext: logContext)
.asyncFlatMapError { error in
let request = UrlNetworkRequest(urlRequest: data)
if error is BaseTechnicalError {
await logContext.add(makeBaseTechinalLog(with: error))
return await cacheReaderNode.process(request, logContext: logContext)
}
await logContext.add(makeLog(with: error, from: request))
return .failure(error)
}
await logContext.add(makeLog(with: error, from: request))
return .failure(error)
}
}
}

// MARK: - Private Method
Expand Down
32 changes: 17 additions & 15 deletions NodeKit/NodeKit/CacheNode/UrlCacheReaderNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,27 @@ open class UrlCacheReaderNode: AsyncNode {
_ data: UrlNetworkRequest,
logContext: LoggingContextProtocol
) async -> NodeResult<Json> {
guard let cachedResponse = extractCachedUrlResponse(data.urlRequest) else {
return .failure(BaseUrlCacheReaderError.cantLoadDataFromCache)
}
await .withCheckedCancellation {
guard let cachedResponse = extractCachedUrlResponse(data.urlRequest) else {
return .failure(BaseUrlCacheReaderError.cantLoadDataFromCache)
}

guard let jsonObjsect = try? JSONSerialization.jsonObject(
with: cachedResponse.data,
options: .allowFragments
) else {
return .failure(BaseUrlCacheReaderError.cantSerializeJson)
}
guard let jsonObjsect = try? JSONSerialization.jsonObject(
with: cachedResponse.data,
options: .allowFragments
) else {
return .failure(BaseUrlCacheReaderError.cantSerializeJson)
}

guard let json = jsonObjsect as? Json else {
guard let json = jsonObjsect as? [Json] else {
return .failure(BaseUrlCacheReaderError.cantCastToJson)
guard let json = jsonObjsect as? Json else {
guard let json = jsonObjsect as? [Json] else {
return .failure(BaseUrlCacheReaderError.cantCastToJson)
}
return .success([MappingUtils.arrayJsonKey: json])
}
return .success([MappingUtils.arrayJsonKey: json])
}

return .success(json)
return .success(json)
}
}

private func extractCachedUrlResponse(_ request: URLRequest) -> CachedURLResponse? {
Expand Down
16 changes: 9 additions & 7 deletions NodeKit/NodeKit/CacheNode/UrlCacheWriterNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ open class UrlCacheWriterNode: AsyncNode {
_ data: UrlProcessedResponse,
logContext: LoggingContextProtocol
) async -> NodeResult<Void> {
let cached = CachedURLResponse(
response: data.response,
data: data.data,
storagePolicy: .allowed
)
URLCache.shared.storeCachedResponse(cached, for: data.request)
return .success(())
await .withCheckedCancellation {
let cached = CachedURLResponse(
response: data.response,
data: data.data,
storagePolicy: .allowed
)
URLCache.shared.storeCachedResponse(cached, for: data.request)
return .success(())
}
}
}
20 changes: 11 additions & 9 deletions NodeKit/NodeKit/CacheNode/UrlNotModifiedTriggerNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,19 @@ open class UrlNotModifiedTriggerNode: AsyncNode {
_ data: UrlDataResponse,
logContext: LoggingContextProtocol
) async -> NodeResult<Json> {
guard data.response.statusCode == 304 else {
await logContext.add(makeErrorLog(code: data.response.statusCode))
return await next.process(data, logContext: logContext)
}
await .withCheckedCancellation {
guard data.response.statusCode == 304 else {
await logContext.add(makeErrorLog(code: data.response.statusCode))
return await next.process(data, logContext: logContext)
}

await logContext.add(makeSuccessLog())
await logContext.add(makeSuccessLog())

return await cacheReader.process(
UrlNetworkRequest(urlRequest: data.request),
logContext: logContext
)
return await cacheReader.process(
UrlNetworkRequest(urlRequest: data.request),
logContext: logContext
)
}
}

// MARK: - Private Methods
Expand Down
19 changes: 19 additions & 0 deletions NodeKit/NodeKit/Core/Node/NodeResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ public extension NodeResult {
}
}

/// Метод вызывает ассинхронную функцию, проверяя жива ли таска.
/// Если таска была отменена, возвращает CancellationError.
///
/// - Parameters:
/// - function: Ассинхронная функция.
/// - Returns: Результат.
@inlinable static func withCheckedCancellation<T>(
_ function: () async -> NodeResult<T>
) async -> NodeResult<T> {
do {
try Task.checkCancellation()
let result = await function()
try Task.checkCancellation()
return result
} catch {
return .failure(error)
}
}

/// Возвращает занчение успешного результата или nil если Failure.
var value: Success? {
switch self {
Expand Down
32 changes: 17 additions & 15 deletions NodeKit/NodeKit/Encodings/UrlJsonRequestEncodingNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,24 @@ open class UrlJsonRequestEncodingNode<Type>: AsyncNode {
_ data: RequestEncodingModel,
logContext: LoggingContextProtocol
) async -> NodeResult<Type> {
var log = getLogMessage(data)
let paramEncoding = parameterEncoding(from: data)
await .withCheckedCancellation {
var log = getLogMessage(data)
let paramEncoding = parameterEncoding(from: data)

guard let encoding = paramEncoding else {
log += "Missed encoding type -> terminate with error"
await logContext.add(log)
return .failure(RequestEncodingNodeError.missedJsonEncodingType)
}
do {
let request = try encoding.encode(urlParameters: data.urlParameters, parameters: data.raw)
log += "type: Json"
return await next.process(request, logContext: logContext)
} catch {
log += "But can't encode data -> terminate with error"
await logContext.add(log)
return .failure(RequestEncodingNodeError.unsupportedDataType)
guard let encoding = paramEncoding else {
log += "Missed encoding type -> terminate with error"
await logContext.add(log)
return .failure(RequestEncodingNodeError.missedJsonEncodingType)
}
do {
let request = try encoding.encode(urlParameters: data.urlParameters, parameters: data.raw)
log += "type: Json"
return await next.process(request, logContext: logContext)
} catch {
log += "But can't encode data -> terminate with error"
await logContext.add(log)
return .failure(RequestEncodingNodeError.unsupportedDataType)
}
}
}

Expand Down
14 changes: 7 additions & 7 deletions NodeKit/NodeKit/Layers/DTOProcessingLayer/DTOMapperNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ open class DTOMapperNode<Input, Output>: AsyncNode where Input: RawEncodable, Ou
_ data: Input,
logContext: LoggingContextProtocol
) async -> NodeResult<Output> {
return await .withMappedExceptions {
let raw = try data.toRaw()
return .success(raw)
await .withMappedExceptions {
return .success(try data.toRaw())
}
.asyncFlatMapError { error in
await log(error: error, logContext: logContext)
return .failure(error)
}
.asyncFlatMap { data in
await next.process(data, logContext: logContext)
.asyncFlatMap { raw in
await .withCheckedCancellation {
await next.process(raw, logContext: logContext)
}
}
.asyncFlatMap { result in
await .withMappedExceptions {
let output = try Output.from(raw: result)
return .success(output)
.success(try Output.from(raw: result))
}
.asyncFlatMapError { error in
await log(error: error, logContext: logContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ open class RawEncoderNode<Input, Output>: AsyncNode where Input: RawEncodable {
_ data: Input,
logContext: LoggingContextProtocol
) async -> NodeResult<Output> {
return await .withMappedExceptions {
return await next.process(try data.toRaw(), logContext: logContext)
await .withMappedExceptions {
.success(try data.toRaw())
}
.asyncFlatMap { raw in
await .withCheckedCancellation {
await next.process(raw, logContext: logContext)
}
}
}
}
Loading

0 comments on commit 79bc0d6

Please sign in to comment.