@@ -388,6 +388,10 @@ const kBoundPath = Symbol('kBoundPath');
388388
389389const isLinux = process . platform === 'linux' ;
390390
391+ // Internal: construct an empty BoundSocket shell during postMessage()
392+ // deserialization; the transferred handle is installed by [kDeserialize].
393+ const kBoundSocketDeserialize = Symbol ( 'kBoundSocketDeserialize' ) ;
394+
391395// A role-neutral wrapper over a synchronously bound libuv handle: bound to a
392396// local address (a numeric IP literal for TCP, or a filesystem/abstract path
393397// for a unix-domain socket via { path }) but neither listening nor connecting
@@ -396,11 +400,20 @@ const isLinux = process.platform === 'linux';
396400// handle must be closed by the caller. bind(2) is non-blocking, so binding
397401// happens inline and errors throw synchronously. No DNS is performed.
398402class BoundSocket {
399- #handle;
403+ #handle = null ;
400404 #address = { } ;
401405 #path;
402406
403407 constructor ( options = kEmptyObject ) {
408+ // An un-adopted BoundSocket can be moved to another thread by listing it
409+ // in the transferList of a worker_threads postMessage() call. See
410+ // [kTransfer]().
411+ markTransferMode ( this , false , true ) ;
412+
413+ if ( options === kBoundSocketDeserialize ) {
414+ return ;
415+ }
416+
404417 validateObject ( options , 'options' ) ;
405418
406419 if ( options . path !== undefined ) {
@@ -544,7 +557,56 @@ class BoundSocket {
544557 get isPipe ( ) {
545558 return this . #path !== undefined ;
546559 }
560+
561+ // A BoundSocket can be transferred only while it still owns its handle,
562+ // i.e. before it has been adopted, closed or already transferred. Only TCP
563+ // binds are transferable; pipe handles cannot move between event loops.
564+ #assertTransferable( ) {
565+ if ( this . #handle === null || ! ( this . #handle instanceof TCP ) ) {
566+ throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE ( 'net.BoundSocket' ) ;
567+ }
568+ }
569+
570+ [ kTransferList ] ( ) {
571+ this . #assertTransferable( ) ;
572+ return [ this . #handle] ;
573+ }
574+
575+ [ kTransfer ] ( ) {
576+ this . #assertTransferable( ) ;
577+ const handle = this . #handle;
578+ // Detach the handle; the messaging layer takes ownership of it via
579+ // TCPWrap::TransferForMessaging(). Further use on the sending side throws
580+ // ERR_SOCKET_HANDLE_ADOPTED, as after adoption.
581+ this . #handle = null ;
582+ return {
583+ data : { handle } ,
584+ deserializeInfo : 'net:_TransferredBoundSocket' ,
585+ } ;
586+ }
587+
588+ [ kDeserialize ] ( data ) {
589+ const handle = data ?. handle ;
590+ if ( handle == null || ! ( handle instanceof TCP ) ) {
591+ throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE ( 'net.BoundSocket' ) ;
592+ }
593+ // Re-derive the bound address from the transferred handle rather than
594+ // trusting serialized state.
595+ const err = handle . getsockname ( this . #address) ;
596+ if ( err ) {
597+ handle . close ( ) ;
598+ throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE ( 'net.BoundSocket' ) ;
599+ }
600+ this . #handle = handle ;
601+ }
602+ }
603+
604+ // Deserialization target for a transferred BoundSocket: constructs an empty
605+ // shell without binding a new socket. Internal, not part of the public API.
606+ function _TransferredBoundSocket ( ) {
607+ return new BoundSocket ( kBoundSocketDeserialize ) ;
547608}
609+ _TransferredBoundSocket . prototype = BoundSocket . prototype ;
548610
549611function Socket ( options ) {
550612 if ( ! ( this instanceof Socket ) ) return new Socket ( options ) ;
@@ -2933,6 +2995,7 @@ Server.prototype.unref = function() {
29332995module . exports = {
29342996 _createServerHandle : createServerHandle ,
29352997 _normalizeArgs : normalizeArgs ,
2998+ _TransferredBoundSocket,
29362999 get BlockList ( ) {
29373000 BlockList ??= require ( 'internal/blocklist' ) . BlockList ;
29383001 return BlockList ;
0 commit comments