File: ExternalReferenceResolverServiceClient.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (138 lines) | stat: -rw-r--r-- 5,357 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2021-2025 Apple Inc. and the Swift project authors
 Licensed under Apache License v2.0 with Runtime Library Exception

 See https://swift.org/LICENSE.txt for license information
 See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import SymbolKit

#if canImport(os)
    import os
    private let logger = Logger(subsystem: "org.swift.docc", category: "ExternalReferenceResolverServiceClient")
#else
    private let logger = NoOpLoggerShim()
#endif

/// A client for performing link resolution requests to a documentation server.
class ExternalReferenceResolverServiceClient {
    /// The maximum amount of time, in seconds, to await a response from the external reference resolver.
    static let responseTimeout = 5
    
    /// The documentation server to which link resolution requests should be sent to.
    var server: DocumentationServer
    
    /// The identifier of the convert request that initiates the reference resolution requests.
    var convertRequestIdentifier: String?
    
    /// The queue on which server messages are awaited.
    var serverResponseQueue = DispatchQueue(
        label: "org.swift.docc.service-external-reference-resolver",
        qos: .unspecified
    )
    
    private var encoder = JSONEncoder()
    
    init(server: DocumentationServer, convertRequestIdentifier: String?) {
        self.server = server
        self.convertRequestIdentifier = convertRequestIdentifier
    }
    
    func sendAndWait(_ request: some Codable) throws -> Data {
        let resultGroup = DispatchGroup()
        
        var result: Result<Data?, Error>?
        
        resultGroup.enter()
        
        serverResponseQueue.async { [weak self] in
            guard let self else { return }
            
            do {
                let encodedRequest = try self.encoder.encode(
                    ConvertRequestContextWrapper(
                        convertRequestIdentifier: self.convertRequestIdentifier,
                        payload: request
                    )
                )
                
                let message = DocumentationServer.Message(
                    type: "resolve-reference",
                    clientName: "SwiftDocC",
                    payload: encodedRequest
                )
                
                let messageData = try self.encoder.encode(message)
                
                self.server.process(messageData) { responseData in
                    defer { resultGroup.leave() }
                    
                    result = self.decodeMessage(responseData).map(\.payload)
                }
            } catch {
                result = .failure(.failedToEncodeRequest(underlyingError: error))
                resultGroup.leave()
            }
        }
        
        guard resultGroup.wait(timeout: .now() + .seconds(Self.responseTimeout)) == .success else {
            logger.log("Timed out when resolving request.")
            throw Error.timeout
        }
        
        switch result {
        case .success(let data?)?:
            return data
        case .success?:
            logger.log("Received nil payload when resolving request.")
            throw Error.missingPayload
        case .failure(let error):
            switch error {
            case .failedToEncodeRequest(let underlyingError):
                logger.log("Unable to encode request for request: \(underlyingError.localizedDescription)")
            case .invalidResponse(let underlyingError):
                logger.log("Received invalid response when resolving request: \(underlyingError.localizedDescription)")
            case .invalidResponseType(let receivedType):
                logger.log("Received unknown response type when resolving request: '\(receivedType)'")
            case .missingPayload:
                logger.log("Received nil payload when resolving request.")
            case .timeout:
                logger.log("Timed out when resolving request.")
            case .receivedErrorFromServer(let message):
                logger.log("Received error from server when resolving request: \(message)")
            case .unknownError:
                logger.log("Unknown error when resolving request.")
            }
            throw error
        case nil:
            logger.log("Unknown error when resolving request.")
            throw Error.unknownError
        }
    }
    
    private func decodeMessage(_ data: Data) -> Result<DocumentationServer.Message, Error> {
        Result {
            try JSONDecoder().decode(DocumentationServer.Message.self, from: data)
        }.mapError { error in
            .invalidResponse(underlyingError: error)
        }.flatMap { message in
            message.type == "resolve-reference-response" ?
                .success(message) :
                .failure(.invalidResponseType(receivedType: message.type.rawValue))
        }
    }

    enum Error: Swift.Error {
        case failedToEncodeRequest(underlyingError: any Swift.Error)
        case invalidResponse(underlyingError: any Swift.Error)
        case invalidResponseType(receivedType: String)
        case missingPayload
        case timeout
        case receivedErrorFromServer(message: String)
        case unknownError
    }
}