Skip to content

Commit f1eafa5

Browse files
Add multi-response test, fix responder delivery race
1 parent 5d0abf1 commit f1eafa5

File tree

2 files changed

+73
-10
lines changed

2 files changed

+73
-10
lines changed

Sources/JSONRPC/JSONRPCSession.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,25 +250,30 @@ extension JSONRPCSession {
250250
}
251251

252252
extension JSONRPCSession {
253-
private func sendDataRequest<Request>(_ params: Request, method: String, responseHandler: @escaping MessageResponder)
254-
where Request: Encodable {
253+
private func sendDataRequest<Request>(
254+
_ params: Request, method: String,
255+
responseHandler: @escaping MessageResponder
256+
) where Request: Encodable {
255257
let issuedId = generateID()
256258

257259
let request = JSONRPCRequest(id: issuedId, method: method, params: params)
258260

261+
// make sure to store the responser *first*, before sending the message. This prevents a race where the response comes in so fast we aren't yet waiting for it
262+
let key = issuedId.description
263+
264+
precondition(responders[key] == nil)
265+
266+
self.responders[key] = responseHandler
267+
259268
Task {
260269
do {
261270
try await encodeAndWrite(request)
262271
} catch {
263272
responseHandler(.failure(error))
273+
274+
self.responders[key] = nil
264275
return
265276
}
266-
267-
let key = issuedId.description
268-
269-
precondition(responders[key] == nil)
270-
271-
self.responders[key] = responseHandler
272277
}
273278
}
274279
}

Tests/JSONRPCTests/JSONRPCSessionTests.swift

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,45 @@ import JSONRPC
33

44
#if compiler(>=5.9)
55

6+
actor PredefinedMessageRelay {
7+
private var messages: [Data]
8+
private let continuation: DataChannel.DataSequence.Continuation
9+
public nonisolated let sequence: DataChannel.DataSequence
10+
11+
init(messages: [Data]) {
12+
self.messages = messages
13+
14+
(self.sequence, self.continuation) = DataChannel.DataSequence.makeStream()
15+
}
16+
17+
init<T: Encodable>(content: [T]) throws {
18+
let messages = try content.map { try JSONEncoder().encode($0) }
19+
20+
self.init(messages: messages)
21+
}
22+
23+
func write() {
24+
continuation.yield(messages.removeFirst())
25+
}
26+
}
27+
28+
extension DataChannel {
29+
static func predefinedMessagesChannel<T: Encodable>(_ content: [T]) throws -> DataChannel {
30+
let relay = try PredefinedMessageRelay(content: content)
31+
32+
return DataChannel(
33+
writeHandler: { _ in
34+
// strong-ref here to keep relay alive
35+
await relay.write()
36+
},
37+
dataSequence: relay.sequence)
38+
}
39+
}
40+
641
final class JSONRPCSessionTests: XCTestCase {
742
typealias TestResponse = JSONRPCResponse<String?>
8-
typealias TestResult = Result<TestResponse, Error>
943

10-
func testThing() async throws {
44+
func testResultResponse() async throws {
1145
let pair = DataChannel.DataSequence.makeStream()
1246

1347
let channel = DataChannel(writeHandler: { _ in },
@@ -27,6 +61,30 @@ final class JSONRPCSessionTests: XCTestCase {
2761

2862
XCTAssertEqual(response.result, "goodbye")
2963
}
64+
65+
func testManySendRequestsWithResponses() async throws {
66+
let iterations = 1000
67+
68+
// be sure to start at 1, to match id generation
69+
let responses = (1...iterations).map { i in
70+
let responseParam = "goodbye-\(i)"
71+
72+
return TestResponse(id: JSONId(i), result: responseParam)
73+
}
74+
75+
let channel = try DataChannel.predefinedMessagesChannel(responses)
76+
let session = JSONRPCSession(channel: channel)
77+
78+
let params = "hello"
79+
80+
for i in 1...iterations {
81+
let expectedResponse = "goodbye-\(i)"
82+
83+
let response: TestResponse = try await session.sendRequest(params, method: "mymethod")
84+
85+
XCTAssertEqual(try! response.content.get(), expectedResponse)
86+
}
87+
}
3088
}
3189

3290
#endif

0 commit comments

Comments
 (0)