From 14b29f8cce70997bbb67d8a70e44a43b2f344981 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Mon, 28 Sep 2026 21:02:38 +0200 Subject: [PATCH] Peer: give a request handler the request's id JSONRPCPeer handed its request handler only the method and params, so a handler could not tell two requests with the same method and params apart, nor tie a request to what it belongs to by its id. setHandlers(identifiedRequest: notification:) installs an IdentifiedRequestHandler, which is given the id of the request it answers as well. setHandlers(request:notification:) is unchanged: its handler is wrapped into the new form. Co-Authored-By: Claude Opus 5.5 --- Sources/JSONRPCPeer/JSONRPCPeer.swift | 23 +++++++++++++--- Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift | 26 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Sources/JSONRPCPeer/JSONRPCPeer.swift b/Sources/JSONRPCPeer/JSONRPCPeer.swift index 6cab9d1..82937ef 100644 --- a/Sources/JSONRPCPeer/JSONRPCPeer.swift +++ b/Sources/JSONRPCPeer/JSONRPCPeer.swift @@ -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 + /// 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 /// Handles an inbound notification (no reply). /// /// Notification handlers are awaited inline on the inbound path to preserve @@ -44,7 +49,7 @@ public actor JSONRPCPeer { private let ownedTransport: JSONRPCMessageTransport? private var nextID = 0 private var pending: [Int: CheckedContinuation] = [:] - private var requestHandler: RequestHandler? + private var requestHandler: IdentifiedRequestHandler? private var notificationHandler: NotificationHandler? private var wireLog: (@Sendable (WireDirection, JSONRPCMessage) -> Void)? private var readTask: Task? @@ -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 } @@ -206,7 +223,7 @@ public actor JSONRPCPeer { Task { [weak self] in let outcome: Result if let handler { - outcome = await handler(method, params) + outcome = await handler(id, method, params) } else { outcome = .success(.null) } diff --git a/Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift b/Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift index 6942796..9ea2a42 100644 --- a/Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift +++ b/Tests/JSONRPCPeerTests/JSONRPCPeerTests.swift @@ -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()