@@ -3,6 +3,7 @@ import os from 'os'
3
3
import { defaultLogger } from '@libp2p/logger'
4
4
import { expect } from 'aegir/chai'
5
5
import defer from 'p-defer'
6
+ import Sinon from 'sinon'
6
7
import { toMultiaddrConnection } from '../src/socket-to-conn.js'
7
8
8
9
async function setup ( opts ?: { server ?: ServerOpts , client ?: SocketConstructorOpts } ) : Promise < { server : Server , serverSocket : Socket , clientSocket : Socket } > {
@@ -287,6 +288,66 @@ describe('socket-to-conn', () => {
287
288
expect ( serverSocket . destroyed ) . to . be . true ( )
288
289
} )
289
290
291
+ it ( 'should not close MultiaddrConnection twice' , async ( ) => {
292
+ ( { server, clientSocket, serverSocket } = await setup ( ) )
293
+ // proxyServerSocket.writableLength returns 100 which cause socket cannot be destroyed immediately
294
+ const proxyServerSocket = new Proxy ( serverSocket , {
295
+ get ( target , prop , receiver ) {
296
+ if ( prop === 'writableLength' ) {
297
+ return 100
298
+ }
299
+ return Reflect . get ( target , prop , receiver )
300
+ }
301
+ } )
302
+
303
+ // spy on `.destroy()` invocations
304
+ const serverSocketDestroySpy = Sinon . spy ( serverSocket , 'destroy' )
305
+ // promise that is resolved when our outgoing socket is closed
306
+ const serverClosed = defer < boolean > ( )
307
+ const socketCloseTimeout = 10
308
+
309
+ const inboundMaConn = toMultiaddrConnection ( proxyServerSocket , {
310
+ socketInactivityTimeout : 100 ,
311
+ socketCloseTimeout,
312
+ logger : defaultLogger ( )
313
+ } )
314
+ expect ( inboundMaConn . timeline . open ) . to . be . ok ( )
315
+ expect ( inboundMaConn . timeline . close ) . to . not . be . ok ( )
316
+
317
+ clientSocket . once ( 'error' , ( ) => { } )
318
+
319
+ serverSocket . once ( 'close' , ( ) => {
320
+ serverClosed . resolve ( true )
321
+ } )
322
+
323
+ // send some data between the client and server
324
+ clientSocket . write ( 'hello' )
325
+ serverSocket . write ( 'goodbye' )
326
+
327
+ const signal = AbortSignal . timeout ( socketCloseTimeout )
328
+ const addEventListenerSpy = Sinon . spy ( signal , 'addEventListener' )
329
+
330
+ // the 2nd and 3rd call should return immediately
331
+ await Promise . all ( [
332
+ inboundMaConn . close ( { signal } ) ,
333
+ inboundMaConn . close ( { signal } ) ,
334
+ inboundMaConn . close ( { signal } )
335
+ ] )
336
+
337
+ // server socket was closed for reading and writing
338
+ await expect ( serverClosed . promise ) . to . eventually . be . true ( )
339
+
340
+ // the connection closing was recorded
341
+ expect ( inboundMaConn . timeline . close ) . to . be . a ( 'number' )
342
+
343
+ // server socket is destroyed
344
+ expect ( serverSocket . destroyed ) . to . be . true ( )
345
+
346
+ // the server socket was only closed once
347
+ expect ( serverSocketDestroySpy . callCount ) . to . equal ( 1 )
348
+ expect ( addEventListenerSpy . callCount ) . to . equal ( 1 )
349
+ } )
350
+
290
351
it ( 'should destroy a socket by timeout when containing MultiaddrConnection is closed' , async ( ) => {
291
352
( { server, clientSocket, serverSocket } = await setup ( {
292
353
server : {
0 commit comments