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

SPT-1998 Swift cuncurrency support #122

Merged
merged 9 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
40 changes: 40 additions & 0 deletions NodeKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
502F9D972BAA36CF00151A8D /* LoggingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F9D962BAA36CF00151A8D /* LoggingContext.swift */; };
502F9D9A2BAA389500151A8D /* LoggingContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F9D992BAA389500151A8D /* LoggingContextTests.swift */; };
502F9D9D2BAA39F400151A8D /* Log+Equatalbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F9D9C2BAA39F400151A8D /* Log+Equatalbe.swift */; };
502F9DA92BAB0CF000151A8D /* TokenRefresherActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F9DA82BAB0CF000151A8D /* TokenRefresherActor.swift */; };
50528E272BADF64F00E86CB6 /* NodeResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E262BADF64F00E86CB6 /* NodeResult.swift */; };
50528E292BAE162600E86CB6 /* LoggerStreamNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E282BAE162600E86CB6 /* LoggerStreamNode.swift */; };
50528E2C2BAE18F400E86CB6 /* AsyncNodeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E2B2BAE18F400E86CB6 /* AsyncNodeMock.swift */; };
50528E312BAE1F9800E86CB6 /* LoggingContextMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E302BAE1F9800E86CB6 /* LoggingContextMock.swift */; };
50528E332BAE295D00E86CB6 /* TokenRefresherActorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E322BAE295D00E86CB6 /* TokenRefresherActorMock.swift */; };
50528E372BAE34A400E86CB6 /* TokenRefresherActorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E362BAE34A400E86CB6 /* TokenRefresherActorTests.swift */; };
50528E392BAE39AE00E86CB6 /* AborterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50528E382BAE39AE00E86CB6 /* AborterMock.swift */; };
90B608D4283E1110006F4309 /* NodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90B608CB283E1110006F4309 /* NodeKit.framework */; };
90B608DA283E1110006F4309 /* NodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 90B608CE283E1110006F4309 /* NodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
90B6090E283E1268006F4309 /* UrlCacheReaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B608E4283E1268006F4309 /* UrlCacheReaderNode.swift */; };
Expand Down Expand Up @@ -165,6 +173,14 @@
502F9D962BAA36CF00151A8D /* LoggingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingContext.swift; sourceTree = "<group>"; };
502F9D992BAA389500151A8D /* LoggingContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingContextTests.swift; sourceTree = "<group>"; };
502F9D9C2BAA39F400151A8D /* Log+Equatalbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Log+Equatalbe.swift"; sourceTree = "<group>"; };
502F9DA82BAB0CF000151A8D /* TokenRefresherActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefresherActor.swift; sourceTree = "<group>"; };
50528E262BADF64F00E86CB6 /* NodeResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeResult.swift; sourceTree = "<group>"; };
50528E282BAE162600E86CB6 /* LoggerStreamNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerStreamNode.swift; sourceTree = "<group>"; };
50528E2B2BAE18F400E86CB6 /* AsyncNodeMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncNodeMock.swift; sourceTree = "<group>"; };
50528E302BAE1F9800E86CB6 /* LoggingContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingContextMock.swift; sourceTree = "<group>"; };
50528E322BAE295D00E86CB6 /* TokenRefresherActorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefresherActorMock.swift; sourceTree = "<group>"; };
50528E362BAE34A400E86CB6 /* TokenRefresherActorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRefresherActorTests.swift; sourceTree = "<group>"; };
50528E382BAE39AE00E86CB6 /* AborterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AborterMock.swift; sourceTree = "<group>"; };
90B608CB283E1110006F4309 /* NodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
90B608CE283E1110006F4309 /* NodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NodeKit.h; sourceTree = "<group>"; };
90B608D3283E1110006F4309 /* NodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -354,6 +370,7 @@
children = (
502F9D9B2BAA39E700151A8D /* Equatable */,
502F9D992BAA389500151A8D /* LoggingContextTests.swift */,
50528E302BAE1F9800E86CB6 /* LoggingContextMock.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand All @@ -366,6 +383,14 @@
path = Equatable;
sourceTree = "<group>";
};
50528E2A2BAE18DD00E86CB6 /* Node */ = {
isa = PBXGroup;
children = (
50528E2B2BAE18F400E86CB6 /* AsyncNodeMock.swift */,
);
path = Node;
sourceTree = "<group>";
};
90B608C1283E1110006F4309 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -495,6 +520,7 @@
isa = PBXGroup;
children = (
90B60900283E1268006F4309 /* Node.swift */,
50528E262BADF64F00E86CB6 /* NodeResult.swift */,
);
path = Node;
sourceTree = "<group>";
Expand Down Expand Up @@ -628,6 +654,7 @@
children = (
90B6094E283E1287006F4309 /* TokenRefresherNode.swift */,
90B6094F283E1287006F4309 /* AccessSafeNode.swift */,
502F9DA82BAB0CF000151A8D /* TokenRefresherActor.swift */,
);
path = AccessSafe;
sourceTree = "<group>";
Expand Down Expand Up @@ -781,6 +808,7 @@
90B60981283E1287006F4309 /* LoggerExtensions.swift */,
90B60982283E1287006F4309 /* Log.swift */,
502F9D962BAA36CF00151A8D /* LoggingContext.swift */,
50528E282BAE162600E86CB6 /* LoggerStreamNode.swift */,
);
path = Logging;
sourceTree = "<group>";
Expand All @@ -796,6 +824,7 @@
90B609C3283E16DC006F4309 /* UnitTests */ = {
isa = PBXGroup;
children = (
50528E2A2BAE18DD00E86CB6 /* Node */,
90B609C4283E16DC006F4309 /* DTO */,
90B609ED283E16DC006F4309 /* AborterNode */,
90B609C6283E16DC006F4309 /* AsyncIterator */,
Expand Down Expand Up @@ -865,6 +894,8 @@
isa = PBXGroup;
children = (
90B609D3283E16DC006F4309 /* TokenRefresherNodeTests.swift */,
50528E322BAE295D00E86CB6 /* TokenRefresherActorMock.swift */,
50528E362BAE34A400E86CB6 /* TokenRefresherActorTests.swift */,
);
path = TokenRefresher;
sourceTree = "<group>";
Expand Down Expand Up @@ -950,6 +981,7 @@
isa = PBXGroup;
children = (
90B609EE283E16DC006F4309 /* AbortingTests.swift */,
50528E382BAE39AE00E86CB6 /* AborterMock.swift */,
);
path = AborterNode;
sourceTree = "<group>";
Expand Down Expand Up @@ -1140,6 +1172,7 @@
90B60995283E1287006F4309 /* DTOEncoderNode.swift in Sources */,
390E69722A136586007F2304 /* UrlJsonRequestEncodingNode.swift in Sources */,
90B60914283E1268006F4309 /* ETagConstants.swift in Sources */,
50528E292BAE162600E86CB6 /* LoggerStreamNode.swift in Sources */,
90B609A2283E1287006F4309 /* ResponseDataParserNode.swift in Sources */,
90B609A8283E1287006F4309 /* TechnicaErrorMapperNode.swift in Sources */,
90B60998283E1287006F4309 /* ModelInputNode.swift in Sources */,
Expand All @@ -1149,6 +1182,7 @@
90B6091A283E1268006F4309 /* RawMappable+Dictionary.swift in Sources */,
90B609A7283E1287006F4309 /* RawEncoderNode.swift in Sources */,
90B609B9283E1287006F4309 /* OffsetAsyncPager.swift in Sources */,
50528E272BADF64F00E86CB6 /* NodeResult.swift in Sources */,
390E697D2A13660C007F2304 /* HTTPHeaders.swift in Sources */,
90B60929283E1268006F4309 /* Context.swift in Sources */,
390E697B2A13660C007F2304 /* MultipartFormData.swift in Sources */,
Expand All @@ -1173,6 +1207,7 @@
90B60915283E1268006F4309 /* UrlETagReaderNode.swift in Sources */,
90B60985283E1287006F4309 /* UrlRequestTrasformatorNode.swift in Sources */,
390E69702A136586007F2304 /* NodeParameterEncoding.swift in Sources */,
502F9DA92BAB0CF000151A8D /* TokenRefresherActor.swift in Sources */,
90B60928283E1268006F4309 /* CancelableContext.swift in Sources */,
90B609C1283E1287006F4309 /* Log.swift in Sources */,
90B60924283E1268006F4309 /* Throttler.swift in Sources */,
Expand All @@ -1194,13 +1229,16 @@
90B60A06283E16DC006F4309 /* AuthModel.swift in Sources */,
90B60A04283E16DC006F4309 /* AuthModelEntry.swift in Sources */,
90B60A01283E16DC006F4309 /* LoggingTests.swift in Sources */,
50528E312BAE1F9800E86CB6 /* LoggingContextMock.swift in Sources */,
90B609FC283E16DC006F4309 /* UrlETagSaverNodeTests.swift in Sources */,
90B609FF283E16DC006F4309 /* UrlETagUrlCacheTriggerNodeTests.swift in Sources */,
90B60A02283E16DC006F4309 /* Infrastructure.swift in Sources */,
50528E332BAE295D00E86CB6 /* TokenRefresherActorMock.swift in Sources */,
90B609F3283E16DC006F4309 /* URLQueryDictionaryKeyEncodingDefaultStrategyTests.swift in Sources */,
90B609F8283E16DC006F4309 /* EncodingTests.swift in Sources */,
90B609F1283E16DC006F4309 /* ChainConfiguratorNodeTests.swift in Sources */,
90B609F6283E16DC006F4309 /* URLQueryInjectorNodeTests.swift in Sources */,
50528E2C2BAE18F400E86CB6 /* AsyncNodeMock.swift in Sources */,
502F9D9A2BAA389500151A8D /* LoggingContextTests.swift in Sources */,
90B609FD283E16DC006F4309 /* UrlETagReaderNodeTests.swift in Sources */,
90B609F7283E16DC006F4309 /* TokenRefresherNodeTests.swift in Sources */,
Expand All @@ -1212,9 +1250,11 @@
90B60A08283E16DC006F4309 /* Credentials.swift in Sources */,
90B609F0283E16DC006F4309 /* OffsetAsyncPagerTests.swift in Sources */,
90B60A00283E16DC006F4309 /* IfConnectionFailedFromCacheNodeTests.swift in Sources */,
50528E372BAE34A400E86CB6 /* TokenRefresherActorTests.swift in Sources */,
90B60A03283E16DC006F4309 /* CredentialsEntry.swift in Sources */,
90B609F2283E16DC006F4309 /* MockerProxyConfigNodeTests.swift in Sources */,
90B609FA283E16DC006F4309 /* FirstCachePolicyTests.swift in Sources */,
50528E392BAE39AE00E86CB6 /* AborterMock.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
27 changes: 26 additions & 1 deletion NodeKit/CacheNode/ETag/UrlETagReaderNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// Этот узел читает eTag-токен из хранилища и добавляет его к запросу.
open class UrlETagReaderNode: Node {
open class UrlETagReaderNode: AsyncNode {

// Следующий узел для обработки.
public var next: any TransportLayerNode
Expand Down Expand Up @@ -48,4 +48,29 @@ open class UrlETagReaderNode: Node {
return next.process(newData)
}

/// Пытается прочесть eTag-токен из хранилища и добавить его к запросу.
/// В случае, если прочесть токен не удалось, то управление просто передается дальше.
open func process(
_ 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)
}

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

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

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

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

}
26 changes: 22 additions & 4 deletions NodeKit/CacheNode/ETag/UrlETagSaverNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension UserDefaults {

/// Этот узел сохраняет пришедшие eTag-токены.
/// В качестве ключа используется абсолютный URL до endpoint-a.
open class UrlETagSaverNode: Node {
open class UrlETagSaverNode: AsyncNode {

/// Следующий узел для обработки.
public var next: (any ResponsePostprocessorLayerNode)?
Expand All @@ -39,20 +39,38 @@ open class UrlETagSaverNode: Node {
self.eTagHeaderKey = eTagHeaderKey
}

/// Пытается получить eTag-токен по ключу `UrlETagSaverNode.eTagHeaderKey`.
/// Пытается получить eTag-токен по ключу.
/// В любом случае передает управление дальше.
open func process(_ data: UrlProcessedResponse) -> Observer<Void> {
guard let tag = data.response.allHeaderFields[self.eTagHeaderKey] as? String,
let url = data.request.url,
let urlAsKey = url.withOrderedQuery()
else {
return .emit(data: ())
return next?.process(data) ?? .emit(data: ())
}

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

return next?.process(data) ?? .emit(data: ())
}

/// Пытается получить eTag-токен по ключу.
/// В любом случае передает управление дальше.
open func process(
_ 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()
chausovSurfStudio marked this conversation as resolved.
Show resolved Hide resolved
else {
return await next?.process(data, logContext: logContext) ?? .success(())
}

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

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

public extension URL {
Expand All @@ -62,7 +80,7 @@ public extension URL {
/// Если параметров нет - возвращает `self.absoluteString`
/// Если параметры есть - сортирует их соединяет в одну строку
/// Удаляет query параметры из исходного URL
/// Склеивает строкое представление URL без парамтеров со сторокой параметров
/// Склеивает строковое представление URL без парамтеров со сторокой параметров
///
/// **ВАЖНО**
///
Expand Down
30 changes: 25 additions & 5 deletions NodeKit/CacheNode/FirstCachePolicyNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ public enum BaseFirstCachePolicyNodeError: Error {
/// Этот узел реализует политику кэширования
/// "Сначала читаем из кэша, а затем запрашиваем у сервера"
/// - Important: В ообщем случае слушатель может быть оповещен дважды. Первый раз, когда ответ прочитан из кэша, а второй раз, когда он был получен с сервера.
open class FirstCachePolicyNode: Node {

open class FirstCachePolicyNode: AsyncStreamNode {
// MARK: - Nested

/// Тип для читающего из URL кэша узла
public typealias CacheReaderNode = Node<UrlNetworkRequest, Json>
public typealias CacheReaderNode = AsyncNode<UrlNetworkRequest, Json>

/// Тип для следующего узла
public typealias NextProcessorNode = Node<RawUrlRequest, Json>
public typealias NextProcessorNode = AsyncNode<RawUrlRequest, Json>

// MARK: - Properties

Expand Down Expand Up @@ -71,5 +70,26 @@ open class FirstCachePolicyNode: Node {

return result
}


/// Пытается получить `URLRequest` и если удается, то обращается в кэш
/// а затем, передает управление следующему узлу.
/// В случае, если получить `URLRequest` не удалось,
/// то управление просто передается следующему узлу
public func process(
_ data: RawUrlRequest,
logContext: LoggingContextProtocol
) -> AsyncStream<NodeResult<Json>> {
return AsyncStream { continuation in
Task {
if let request = data.toUrlRequest() {
let cacheResult = await cacheReaderNode.process(request, logContext: logContext)
continuation.yield(cacheResult)
}

let nextResult = await next.process(data, logContext: logContext)
continuation.yield(nextResult)
continuation.finish()
}
}
}
}
49 changes: 45 additions & 4 deletions NodeKit/CacheNode/IfServerFailsFromCacheNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ import Foundation

/// Узел реализует политику кэширования "Если интернета нет, то запросить данные из кэша"
/// Этот узел работает с URL кэшом.
open class IfConnectionFailedFromCacheNode: Node {
open class IfConnectionFailedFromCacheNode: AsyncNode {

/// Следующий узел для обработки.
public var next: any Node<URLRequest, Json>
public var next: any AsyncNode<URLRequest, Json>
/// Узел, считывающий данные из URL кэша.
public var cacheReaderNode: any Node<UrlNetworkRequest, Json>
public var cacheReaderNode: any AsyncNode<UrlNetworkRequest, Json>

/// Инициаллизирует узел.
///
/// - Parameters:
/// - next: Следующий узел для обработки.
/// - cacheReaderNode: Узел, считывающий данные из URL кэша.
public init(next: any Node<URLRequest, Json>, cacheReaderNode: any Node<UrlNetworkRequest, Json>) {
public init(next: any AsyncNode<URLRequest, Json>, cacheReaderNode: any AsyncNode<UrlNetworkRequest, Json>) {
self.next = next
self.cacheReaderNode = cacheReaderNode
}
Expand All @@ -47,4 +47,45 @@ open class IfConnectionFailedFromCacheNode: Node {
}
}

/// Проверяет, произошла ли ошибка связи в ответ на запрос.
/// Если ошибка произошла, то возвращает успешный ответ из кэша.
/// В противном случае передает управление следующему узлу.
open func process(
_ 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 logContext.add(makeLog(with: error, from: request))
return .failure(error)
}
}

// MARK: - Private Method

private func makeBaseTechinalLog(with error: Error) -> Log {
return Log(
logViewObjectName +
"Catching \(error)" + .lineTabDeilimeter +
"Start read cache" + .lineTabDeilimeter,
id: objectName
)
}

private func makeLog(with error: Error, from request: UrlNetworkRequest) -> Log {
return Log(
logViewObjectName +
"Catching \(error)" + .lineTabDeilimeter +
"Error is \(type(of: error))" +
"and request = \(String(describing: request))" + .lineTabDeilimeter +
"-> throw error",
id: objectName
)
}

}
Loading
Loading