Skip to content
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
23 changes: 20 additions & 3 deletions Sources/JSONRPCPeer/JSONRPCPeer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public actor JSONRPCPeer {
/// Handles an inbound (peer-originated) request; returns the result or an error.
public typealias RequestHandler =
@Sendable (_ method: String, _ params: JSONValue?) async -> Result<JSONValue, JSONRPCError>
/// Handles an inbound request given its `id` as well, for a handler that must tell one
/// request from another with the same method and params, or tie a request to others by
/// its id. See ``setHandlers(identifiedRequest:notification:)``.
public typealias IdentifiedRequestHandler =
@Sendable (_ id: JSONRPCID, _ method: String, _ params: JSONValue?) async -> Result<JSONValue, JSONRPCError>
/// Handles an inbound notification (no reply).
///
/// Notification handlers are awaited inline on the inbound path to preserve
Expand All @@ -44,7 +49,7 @@ public actor JSONRPCPeer {
private let ownedTransport: JSONRPCMessageTransport?
private var nextID = 0
private var pending: [Int: CheckedContinuation<JSONValue, Error>] = [:]
private var requestHandler: RequestHandler?
private var requestHandler: IdentifiedRequestHandler?
private var notificationHandler: NotificationHandler?
private var wireLog: (@Sendable (WireDirection, JSONRPCMessage) -> Void)?
private var readTask: Task<Void, Never>?
Expand All @@ -67,7 +72,19 @@ public actor JSONRPCPeer {
/// request handler, inbound requests are acknowledged with a null success so a
/// peer that required a reply isn't left waiting.
public func setHandlers(request: RequestHandler?, notification: NotificationHandler?) {
self.requestHandler = request
if let request {
self.requestHandler = { _, method, params in await request(method, params) }
} else {
self.requestHandler = nil
}
self.notificationHandler = notification
}

/// Install handlers as ``setHandlers(request:notification:)`` does, with a request handler
/// that is given each request's `id` too: the id of the message it answers, which the
/// wire log (``setWireLog(_:)``) showed as the request was read.
public func setHandlers(identifiedRequest: IdentifiedRequestHandler?, notification: NotificationHandler?) {
self.requestHandler = identifiedRequest
self.notificationHandler = notification
}

Expand Down Expand Up @@ -206,7 +223,7 @@ public actor JSONRPCPeer {
Task { [weak self] in
let outcome: Result<JSONValue, JSONRPCError>
if let handler {
outcome = await handler(method, params)
outcome = await handler(id, method, params)
} else {
outcome = .success(.null)
}
Expand Down
26 changes: 26 additions & 0 deletions Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,32 @@ func dispatchesInboundRequestsAndRepliesWithHandlerResult() async {
await peer.close()
}

/// A handler installed with `identifiedRequest:` is given each request's id, so two requests
/// alike in method and params are told apart: each is answered with what its own id gave.
@Test(.timeLimit(.minutes(1)))
func givesAnIdentifiedRequestHandlerEachRequestsID() async {
let transport = SpyTransport()
let peer = JSONRPCPeer(transport: transport)
await peer.setHandlers(
identifiedRequest: { id, method, _ in
switch id {
case .integer(let value): return .success(.string("\(method)#\(value)"))
case .string(let value): return .success(.string("\(method)#\(value)"))
}
},
notification: nil)
await peer.start()

let params: JSONValue = .object(["path": .string("/tmp/a.txt")])
let first = await injectAndAwaitReply(transport, .request(id: 1, method: "fs/read", params: params))
let second = await injectAndAwaitReply(transport, .request(id: "two", method: "fs/read", params: params))
#expect(first.id == 1)
#expect(first.result?.stringValue == "fs/read#1")
#expect(second.id == "two")
#expect(second.result?.stringValue == "fs/read#two")
await peer.close()
}

@Test(.timeLimit(.minutes(1)))
func acknowledgesInboundRequestsWithNullWhenNoHandler() async {
let transport = SpyTransport()
Expand Down
Loading