1+ import { describe , expect , test , beforeEach } from "@jest/globals" ;
2+ import { Protocol } from "./protocol.js" ;
3+ import { Transport } from "./transport.js" ;
4+ import { Request , Notification , Result , JSONRPCMessage } from "../types.js" ;
5+ import { z } from "zod" ;
6+
7+ // Mock Transport class
8+ class MockTransport implements Transport {
9+ id : string ;
10+ onclose ?: ( ) => void ;
11+ onerror ?: ( error : Error ) => void ;
12+ onmessage ?: ( message : unknown ) => void ;
13+ sentMessages : JSONRPCMessage [ ] = [ ] ;
14+
15+ constructor ( id : string ) {
16+ this . id = id ;
17+ }
18+
19+ async start ( ) : Promise < void > { }
20+
21+ async close ( ) : Promise < void > {
22+ this . onclose ?.( ) ;
23+ }
24+
25+ async send ( message : JSONRPCMessage ) : Promise < void > {
26+ this . sentMessages . push ( message ) ;
27+ }
28+ }
29+
30+ describe ( "Protocol transport handling bug" , ( ) => {
31+ let protocol : Protocol < Request , Notification , Result > ;
32+ let transportA : MockTransport ;
33+ let transportB : MockTransport ;
34+
35+ beforeEach ( ( ) => {
36+ protocol = new ( class extends Protocol < Request , Notification , Result > {
37+ protected assertCapabilityForMethod ( ) : void { }
38+ protected assertNotificationCapability ( ) : void { }
39+ protected assertRequestHandlerCapability ( ) : void { }
40+ } ) ( ) ;
41+
42+ transportA = new MockTransport ( "A" ) ;
43+ transportB = new MockTransport ( "B" ) ;
44+ } ) ;
45+
46+ test ( "should send response to the correct transport when multiple clients are connected" , async ( ) => {
47+ // Set up a request handler that simulates processing time
48+ let resolveHandler : ( value : Result ) => void ;
49+ const handlerPromise = new Promise < Result > ( ( resolve ) => {
50+ resolveHandler = resolve ;
51+ } ) ;
52+
53+ const TestRequestSchema = z . object ( {
54+ method : z . literal ( "test/method" ) ,
55+ params : z . object ( {
56+ from : z . string ( )
57+ } ) . optional ( )
58+ } ) ;
59+
60+ protocol . setRequestHandler (
61+ TestRequestSchema ,
62+ async ( request ) => {
63+ console . log ( `Processing request from ${ request . params ?. from } ` ) ;
64+ return handlerPromise ;
65+ }
66+ ) ;
67+
68+ // Client A connects and sends a request
69+ await protocol . connect ( transportA ) ;
70+
71+ const requestFromA = {
72+ jsonrpc : "2.0" as const ,
73+ method : "test/method" ,
74+ params : { from : "clientA" } ,
75+ id : 1
76+ } ;
77+
78+ // Simulate client A sending a request
79+ transportA . onmessage ?.( requestFromA ) ;
80+
81+ // While A's request is being processed, client B connects
82+ // This overwrites the transport reference in the protocol
83+ await protocol . connect ( transportB ) ;
84+
85+ const requestFromB = {
86+ jsonrpc : "2.0" as const ,
87+ method : "test/method" ,
88+ params : { from : "clientB" } ,
89+ id : 2
90+ } ;
91+
92+ // Client B sends its own request
93+ transportB . onmessage ?.( requestFromB ) ;
94+
95+ // Now complete A's request
96+ resolveHandler ! ( { data : "responseForA" } as Result ) ;
97+
98+ // Wait for async operations to complete
99+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
100+
101+ // Check where the responses went
102+ console . log ( "Transport A received:" , transportA . sentMessages ) ;
103+ console . log ( "Transport B received:" , transportB . sentMessages ) ;
104+
105+ // FIXED: Each transport now receives its own response
106+
107+ // Transport A should receive response for request ID 1
108+ expect ( transportA . sentMessages . length ) . toBe ( 1 ) ;
109+ expect ( transportA . sentMessages [ 0 ] ) . toMatchObject ( {
110+ jsonrpc : "2.0" ,
111+ id : 1 ,
112+ result : { data : "responseForA" }
113+ } ) ;
114+
115+ // Transport B should only receive its own response (when implemented)
116+ expect ( transportB . sentMessages . length ) . toBe ( 1 ) ;
117+ expect ( transportB . sentMessages [ 0 ] ) . toMatchObject ( {
118+ jsonrpc : "2.0" ,
119+ id : 2 ,
120+ result : { data : "responseForA" } // Same handler result in this test
121+ } ) ;
122+ } ) ;
123+
124+ test ( "demonstrates the timing issue with multiple rapid connections" , async ( ) => {
125+ const delays : number [ ] = [ ] ;
126+ const results : { transport : string ; response : JSONRPCMessage [ ] } [ ] = [ ] ;
127+
128+ const DelayedRequestSchema = z . object ( {
129+ method : z . literal ( "test/delayed" ) ,
130+ params : z . object ( {
131+ delay : z . number ( ) ,
132+ client : z . string ( )
133+ } ) . optional ( )
134+ } ) ;
135+
136+ // Set up handler with variable delay
137+ protocol . setRequestHandler (
138+ DelayedRequestSchema ,
139+ async ( request , extra ) => {
140+ const delay = request . params ?. delay || 0 ;
141+ delays . push ( delay ) ;
142+
143+ await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
144+
145+ return {
146+ processedBy : `handler-${ extra . requestId } ` ,
147+ delay : delay
148+ } as Result ;
149+ }
150+ ) ;
151+
152+ // Rapid succession of connections and requests
153+ await protocol . connect ( transportA ) ;
154+ transportA . onmessage ?.( {
155+ jsonrpc : "2.0" as const ,
156+ method : "test/delayed" ,
157+ params : { delay : 50 , client : "A" } ,
158+ id : 1
159+ } ) ;
160+
161+ // Connect B while A is processing
162+ setTimeout ( async ( ) => {
163+ await protocol . connect ( transportB ) ;
164+ transportB . onmessage ?.( {
165+ jsonrpc : "2.0" as const ,
166+ method : "test/delayed" ,
167+ params : { delay : 10 , client : "B" } ,
168+ id : 2
169+ } ) ;
170+ } , 10 ) ;
171+
172+ // Wait for all processing
173+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
174+
175+ // Collect results
176+ if ( transportA . sentMessages . length > 0 ) {
177+ results . push ( { transport : "A" , response : transportA . sentMessages } ) ;
178+ }
179+ if ( transportB . sentMessages . length > 0 ) {
180+ results . push ( { transport : "B" , response : transportB . sentMessages } ) ;
181+ }
182+
183+ console . log ( "Timing test results:" , results ) ;
184+
185+ // FIXED: Each transport receives its own responses
186+ expect ( transportA . sentMessages . length ) . toBe ( 1 ) ;
187+ expect ( transportB . sentMessages . length ) . toBe ( 1 ) ;
188+ } ) ;
189+ } ) ;
0 commit comments