@gjsify/net 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/server.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{Socket as e}from"./socket.js";import t from"@girs/gio-2.0";import{EventEmitter as n}from"node:events";import{createNodeError as r,deferEmit as i,ensureMainLoop as a}from"@gjsify/utils/core";const o=new Set;var Server=class extends n{listening=!1;maxConnections;allowHalfOpen;_service=null;_connections=new Set;_address=null;constructor(e,t){super(),typeof e==`function`?(t=e,this.allowHalfOpen=!1):this.allowHalfOpen=e?.allowHalfOpen??!1,t&&this.on(`connection`,t)}listen(...e){let n=0,s=`0.0.0.0`,c=511,l;if(typeof e[0]==`object`&&e[0]!==null&&!Array.isArray(e[0])){let t=e[0];n=t.port??0,s=t.host??`0.0.0.0`,c=t.backlog??511,l=e[1]}else{typeof e[0]==`number`&&(n=e[0]);for(let t=1;t<e.length;t++)typeof e[t]==`string`?s=e[t]:typeof e[t]==`number`?c=e[t]:typeof e[t]==`function`&&(l=e[t])}l&&this.once(`listening`,l);try{this._service=new t.SocketService,this._service.set_backlog(c);let e;n===0?e=this._service.add_any_inet_port(null):(this._service.add_inet_port(n,null),e=n),this._service.connect(`incoming`,(e,t)=>(this._handleConnection(t),!0)),this._service.start(),a(),this.listening=!0,o.add(this);let r=s.includes(`:`)?`IPv6`:`IPv4`;this._address={port:e,family:r,address:s},i(this,`listening`)}catch(e){let t=r(e,`listen`,{address:s,port:n});i(this,`error`,t)}return this}_handleConnection(t){if(this.maxConnections&&this._connections.size>=this.maxConnections){try{t.close(null)}catch{}return}let n=new e({allowHalfOpen:this.allowHalfOpen});n._setConnection(t),n._setupConnection({}),this._connections.add(n),n.on(`close`,()=>{this._connections.delete(n)}),this.emit(`connection`,n)}address(){return this._address}close(e){if(e&&this.once(`close`,e),!this._service||!this.listening)return setTimeout(()=>{let e=Error(`Server is not running`);e.code=`ERR_SERVER_NOT_RUNNING`,this.emit(`error`,e)},0),this;this._service.stop(),this._service.close(),this._service=null,this.listening=!1,o.delete(this);for(let e of this._connections)e.destroy();return this._connections.clear(),i(this,`close`),this}getConnections(e){e(null,this._connections.size)}ref(){return this}unref(){return this}};export{Server};
1
+ import"./_virtual/_rolldown/runtime.js";import{Socket as e}from"./socket.js";import t from"@girs/gio-2.0";import{closeSocketService as n,releaseListenPort as r}from"@gjsify/utils";import{EventEmitter as i}from"node:events";import{createNodeError as a,deferEmit as o,ensureMainLoop as s,nextTick as c}from"@gjsify/utils/core";const l=new Set;var Server=class extends i{listening=!1;maxConnections;allowHalfOpen;_service=null;_connections=new Set;_closingService=null;_address=null;constructor(e,t){super(),typeof e==`function`?(t=e,this.allowHalfOpen=!1):this.allowHalfOpen=e?.allowHalfOpen??!1,t&&this.on(`connection`,t)}listen(...e){let n=0,i=`0.0.0.0`,c=511,u;if(typeof e[0]==`object`&&e[0]!==null&&!Array.isArray(e[0])){let t=e[0];n=t.port??0,i=t.host??`0.0.0.0`,c=t.backlog??511,u=e[1]}else{typeof e[0]==`number`&&(n=e[0]);for(let t=1;t<e.length;t++)typeof e[t]==`string`?i=e[t]:typeof e[t]==`number`?c=e[t]:typeof e[t]==`function`&&(u=e[t])}u&&this.once(`listening`,u);try{r(n),this._service=new t.SocketService,this._service.set_backlog(c);let e;n===0?e=this._service.add_any_inet_port(null):(this._service.add_inet_port(n,null),e=n),this._service.connect(`incoming`,(e,t)=>(this._handleConnection(t),!0)),this._service.start(),s(),this.listening=!0,l.add(this);let a=i.includes(`:`)?`IPv6`:`IPv4`;this._address={port:e,family:a,address:i},o(this,`listening`)}catch(e){this._service=null;let t=a(e,`listen`,{address:i,port:n});o(this,`error`,t)}return this}_handleConnection(t){if(this.maxConnections&&this._connections.size>=this.maxConnections){try{t.close(null)}catch{}return}let n=new e({allowHalfOpen:this.allowHalfOpen});n._setConnection(t),n._setupConnection({},!1),this._connections.add(n),n.on(`close`,()=>{this._connections.delete(n),this._emitCloseIfDrained()}),this.emit(`connection`,n),n._startReading()}address(){return this._address}close(e){let t=this._service;return e&&this.once(`close`,t?e:()=>{let t=Error(`Server is not running.`);t.code=`ERR_SERVER_NOT_RUNNING`,e(t)}),t&&(this._service=null,this.listening=!1,this._closingService=t,n(t,this._address?[this._address.port]:[],()=>{this._closingService===t&&(this._closingService=null),this._emitCloseIfDrained()})),this._emitCloseIfDrained(),this}_emitCloseIfDrained(){this._service||this._closingService||this._connections.size>0||(l.delete(this),c(()=>this.emit(`close`)))}getConnections(e){e(null,this._connections.size)}ref(){return this}unref(){return this}};export{Server};
package/lib/esm/socket.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import{Duplex as n}from"node:stream";import{Buffer as r}from"node:buffer";import{createNodeError as i,gbytesToUint8Array as a}from"@gjsify/utils";var Socket=class extends n{remoteAddress;remotePort;remoteFamily;localAddress;localPort;bytesRead=0;bytesWritten=0;connecting=!1;destroyed=!1;pending=!0;readyState=`closed`;_connection=null;_ioStream=null;_inputStream=null;_outputStream=null;_cancellable=new e.Cancellable;_reading=!1;_timeout=0;_timeoutId=null;constructor(e){super(e),this.allowHalfOpen=e?.allowHalfOpen??!1}_setConnection(e){this._connection=e}_setupFromIOStream(t){this._ioStream=t;try{let n=t;if(typeof n.get_socket==`function`){this._connection=n;let t=n.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`;let r=n.get_local_address();this.localAddress=r.get_address().to_string(),this.localPort=r.get_port()}}catch{}this._inputStream=t.get_input_stream(),this._outputStream=t.get_output_stream(),this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`),this._startReading()}_attachOutputOnly(t){this._ioStream=t;try{let n=t;if(typeof n.get_socket==`function`){this._connection=n;let t=n.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`;let r=n.get_local_address();this.localAddress=r.get_address().to_string(),this.localPort=r.get_port()}}catch{}this._inputStream=t.get_input_stream(),this._outputStream=t.get_output_stream(),this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`)}_releaseIOStream(){let e=this._ioStream;return this._ioStream=null,this._inputStream=null,this._outputStream=null,this._connection=null,e}connect(t,n,r){let a;typeof t==`number`?(a={port:t,host:typeof n==`string`?n:`localhost`},typeof n==`function`&&(r=n)):(a=t,typeof n==`function`&&(r=n)),r&&this.once(`connect`,r),this.connecting=!0,this.readyState=`opening`,this.pending=!0;let o=a.host||`localhost`,s=a.port,c=new e.SocketClient;return a.timeout&&c.set_timeout(Math.ceil(a.timeout/1e3)),c.connect_to_host_async(o,s,this._cancellable,(e,t)=>{try{this._connection=c.connect_to_host_finish(t),this._setupConnection(a)}catch(e){this.connecting=!1,this.readyState=`closed`;let t=i(e,`connect`,{address:o,port:s});this.destroy(t)}}),this}_setupConnection(t){if(!this._connection)return;let n=this._connection.get_socket();if(this._inputStream=this._connection.get_input_stream(),this._outputStream=this._connection.get_output_stream(),`keepAlive`in t&&t.keepAlive&&n.set_keepalive(!0),!(`noDelay`in t)||t.noDelay!==!1)try{n.set_option(6,1,1)}catch{}try{let t=this._connection.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`}catch{}try{let e=this._connection.get_local_address();this.localAddress=e.get_address().to_string(),this.localPort=e.get_port()}catch{}this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`),this._startReading()}_startReading(){this._reading||!this._inputStream||(this._reading=!0,this._readLoop())}async _readLoop(){let e=this._inputStream;if(e)try{for(;this._reading&&e;){let n=await new Promise((n,r)=>{e.read_bytes_async(16384,t.PRIORITY_DEFAULT,this._cancellable,(t,i)=>{try{n(e.read_bytes_finish(i))}catch(e){r(e)}})});if(!n||n.get_size()===0){this._reading=!1,this.allowHalfOpen?this.readyState=this.writable?`writeOnly`:`closed`:this.once(`end`,()=>{this.end(),this.readyState=`closed`}),this.push(null);break}let i=a(n);if(this.bytesRead+=i.length,this._resetTimeout(),!this.push(r.from(i))){this._reading=!1;break}}}catch(e){this._cancellable.is_cancelled()||this.destroy(i(e,`read`,{address:this.remoteAddress}))}}_read(e){!this._reading&&this._inputStream&&this._startReading()}_write(e,n,a){if(!this._outputStream){a(Error(`Socket is not connected`));return}let o=r.isBuffer(e)?e:typeof e==`string`?r.from(e,n):r.from(e);this._outputStream.write_bytes_async(new t.Bytes(o),t.PRIORITY_DEFAULT,this._cancellable,(e,t)=>{try{let e=this._outputStream.write_bytes_finish(t);this.bytesWritten+=e,this._resetTimeout(),a(null)}catch(e){a(i(e,`write`,{address:this.remoteAddress}))}})}_final(e){if(this._connection)try{this._connection.get_socket().shutdown(!1,!0),this.readyState=this.readable?`readOnly`:`closed`}catch{}else if(this._ioStream){try{this._ioStream.close(null),this.readyState=`closed`}catch{}this._ioStream=null,this._inputStream=null,this._outputStream=null}e()}_destroy(e,t){if(this._reading=!1,this._clearTimeout(),this._cancellable.cancel(),this._connection){try{this._connection.close(null)}catch{}this._connection=null}else if(this._ioStream)try{this._ioStream.close(null)}catch{}this._ioStream=null,this._inputStream=null,this._outputStream=null,this.readyState=`closed`,this.destroyed=!0,t(e)}setTimeout(e,t){return this._timeout=e,this._clearTimeout(),t&&this.once(`timeout`,t),e>0&&this._resetTimeout(),this}_resetTimeout(){this._clearTimeout(),this._timeout>0&&(this._timeoutId=setTimeout(()=>{this.emit(`timeout`)},this._timeout))}_clearTimeout(){this._timeoutId!==null&&(clearTimeout(this._timeoutId),this._timeoutId=null)}setKeepAlive(e,t){return this._connection?.get_socket().set_keepalive(e??!1),this}setNoDelay(e){if(this._connection)try{this._connection.get_socket().set_option(6,1,e===!1?0:1)}catch{}return this}destroySoon(){this.writable&&this.end(),this.writableFinished?this.destroy():this.once(`finish`,this.destroy.bind(this))}address(){return this.localAddress&&this.localPort?{port:this.localPort,family:this.remoteFamily||`IPv4`,address:this.localAddress}:{}}ref(){return this}unref(){return this}};export{Socket};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import{Duplex as n}from"node:stream";import{Buffer as r}from"node:buffer";import{createNodeError as i,gbytesToUint8Array as a}from"@gjsify/utils";function destroyedError(){let e=Error(`Cannot call write after a stream was destroyed`);return e.code=`ERR_STREAM_DESTROYED`,e}var Socket=class extends n{remoteAddress;remotePort;remoteFamily;localAddress;localPort;bytesRead=0;bytesWritten=0;connecting=!1;destroyed=!1;pending=!0;readyState=`closed`;_connection=null;_ioStream=null;_inputStream=null;_outputStream=null;_cancellable=new e.Cancellable;_readCancellable=new e.Cancellable;_reading=!1;_detaching=!1;_detachResolve=null;_pendingIo=0;_afterIo=null;_timeout=0;_timeoutId=null;constructor(e){super(e),this.allowHalfOpen=e?.allowHalfOpen??!1}_setConnection(e){this._connection=e}_setupFromIOStream(t){this._ioStream=t;try{let n=t;if(typeof n.get_socket==`function`){this._connection=n;let t=n.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`;let r=n.get_local_address();this.localAddress=r.get_address().to_string(),this.localPort=r.get_port()}}catch{}this._inputStream=t.get_input_stream(),this._outputStream=t.get_output_stream(),this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`),this._startReading()}_attachOutputOnly(t){this._ioStream=t;try{let n=t;if(typeof n.get_socket==`function`){this._connection=n;let t=n.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`;let r=n.get_local_address();this.localAddress=r.get_address().to_string(),this.localPort=r.get_port()}}catch{}this._inputStream=t.get_input_stream(),this._outputStream=t.get_output_stream(),this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`)}_releaseIOStream(){let e=this._ioStream;return this._ioStream=null,this._inputStream=null,this._outputStream=null,this._connection=null,e}_claimConnection(){let e={connection:this._connection,ioStream:this._ioStream,inputStream:this._inputStream,outputStream:this._outputStream};return this._connection=null,this._ioStream=null,this._inputStream=null,this._outputStream=null,e}connect(t,n,r){let a;typeof t==`number`?(a={port:t,host:typeof n==`string`?n:`localhost`},typeof n==`function`&&(r=n)):(a=t,typeof n==`function`&&(r=n)),r&&this.once(`connect`,r),this.connecting=!0,this.readyState=`opening`,this.pending=!0;let o=a.host||`localhost`,s=a.port,c=new e.SocketClient;return a.timeout&&c.set_timeout(Math.ceil(a.timeout/1e3)),c.connect_to_host_async(o,s,this._cancellable,(e,t)=>{try{let e=c.connect_to_host_finish(t);if(this.destroyed){e.close(null);return}this._connection=e,this._setupConnection(a)}catch(e){this.connecting=!1,this.readyState=`closed`;let t=i(e,`connect`,{address:o,port:s});this.destroy(t)}}),this}_setupConnection(t,n=!0){if(!this._connection)return;let r=this._connection.get_socket();if(this._inputStream=this._connection.get_input_stream(),this._outputStream=this._connection.get_output_stream(),`keepAlive`in t&&t.keepAlive&&r.set_keepalive(!0),!(`noDelay`in t)||t.noDelay!==!1)try{r.set_option(6,1,1)}catch{}try{let t=this._connection.get_remote_address();this.remoteAddress=t.get_address().to_string(),this.remotePort=t.get_port(),this.remoteFamily=t.get_address().get_family()===e.SocketFamily.IPV6?`IPv6`:`IPv4`}catch{}try{let e=this._connection.get_local_address();this.localAddress=e.get_address().to_string(),this.localPort=e.get_port()}catch{}this.connecting=!1,this.pending=!1,this.readyState=`open`,this.emit(`connect`),this.emit(`ready`),n&&this._startReading()}_startReading(){this._reading||!this._inputStream||(this._reading=!0,this._readLoop())}async _readLoop(){let e=this._inputStream;if(e)try{for(;this._reading&&e;){let n=await new Promise((n,r)=>{this._pendingIo++,e.read_bytes_async(16384,t.PRIORITY_DEFAULT,this._readCancellable,(t,i)=>{try{n(e.read_bytes_finish(i))}catch(e){r(e)}finally{this._ioSettled()}})});if(this._detaching){this._resolveDetach(n&&n.get_size()>0?r.from(a(n)):null);return}if(!n||n.get_size()===0){this._reading=!1,this.allowHalfOpen?this.readyState=this.writable?`writeOnly`:`closed`:this.once(`end`,()=>{this.end(),this.readyState=`closed`}),this.push(null),this.read(0);break}let i=a(n);if(this.bytesRead+=i.length,this._resetTimeout(),!this.push(r.from(i))){this._reading=!1;break}}}catch(e){if(this._detaching){this._resolveDetach(null);return}this._readCancellable.is_cancelled()||this.destroy(i(e,`read`,{address:this.remoteAddress}))}}_detachReader(){return this._reading?(this._detaching=!0,this._reading=!1,this._readCancellable.cancel(),new Promise(e=>{this._detachResolve=e})):Promise.resolve(null)}_resolveDetach(e){this._detaching=!1;let t=this._detachResolve;this._detachResolve=null,t&&t(e)}_read(e){!this._reading&&this._inputStream&&this._startReading()}_write(e,n,a){if(!this._outputStream){a(Error(`Socket is not connected`));return}let o=r.isBuffer(e)?e:typeof e==`string`?r.from(e,n):r.from(e),s=this._outputStream;this._pendingIo++,s.write_bytes_async(new t.Bytes(o),t.PRIORITY_DEFAULT,this._cancellable,(e,t)=>{let n=null;try{let e=s.write_bytes_finish(t);this.bytesWritten+=e,this._resetTimeout()}catch(e){n=this._cancellable.is_cancelled()?destroyedError():i(e,`write`,{address:this.remoteAddress})}this._ioSettled(),a(n)})}_final(e){if(this._connection)try{this._connection.get_socket().shutdown(!1,!0),this.readyState=this.readable?`readOnly`:`closed`}catch{}else if(this._ioStream){try{this._ioStream.close(null),this.readyState=`closed`}catch{}this._ioStream=null,this._inputStream=null,this._outputStream=null}e()}_destroy(e,t){this._reading=!1,this._clearTimeout(),this._cancellable.cancel(),this._readCancellable.cancel();let release=()=>this._release(e,t);this._pendingIo===0?release():this._afterIo=release}_ioSettled(){if(this._pendingIo--,this._pendingIo===0&&this._afterIo){let e=this._afterIo;this._afterIo=null,e()}}_release(e,t){if(this._connection){try{this._connection.close(null)}catch{}this._connection=null}else if(this._ioStream)try{this._ioStream.close(null)}catch{}this._ioStream=null,this._inputStream=null,this._outputStream=null,this.readyState=`closed`,this.destroyed=!0,t(e)}setTimeout(e,t){return this._timeout=e,this._clearTimeout(),t&&this.once(`timeout`,t),e>0&&this._resetTimeout(),this}_resetTimeout(){this._clearTimeout(),this._timeout>0&&(this._timeoutId=setTimeout(()=>{this.emit(`timeout`)},this._timeout))}_clearTimeout(){this._timeoutId!==null&&(clearTimeout(this._timeoutId),this._timeoutId=null)}setKeepAlive(e,t){return this._connection?.get_socket().set_keepalive(e??!1),this}setNoDelay(e){if(this._connection)try{this._connection.get_socket().set_option(6,1,e===!1?0:1)}catch{}return this}destroySoon(){this.writable&&this.end(),this.writableFinished?this.destroy():this.once(`finish`,this.destroy.bind(this))}address(){return this.localAddress&&this.localPort?{port:this.localPort,family:this.remoteFamily||`IPv4`,address:this.localAddress}:{}}ref(){return this}unref(){return this}};export{Socket};
@@ -1,2 +1,18 @@
1
+ /**
2
+ * Is this socket error the peer aborting the connection rather than a real fault?
3
+ *
4
+ * Closing a TCP connection that still has unread data queued in the peer's receive
5
+ * buffer makes the stack send RST instead of FIN, and the sender's next read reports
6
+ * `ECONNRESET`. Whether that surfaces depends on the OS, which is why it needs naming
7
+ * rather than blanket tolerance: win32 delivers the reset to a server socket whose
8
+ * client simply walked away, darwin and linux deliver a clean FIN and the read reports
9
+ * EOF. `read ECONNRESET` is that event, not a bug — a spec that has no reader for the
10
+ * peer's data provokes it.
11
+ *
12
+ * Deliberately narrow: `EPIPE` is OUR write to a socket the peer already closed, and a
13
+ * `ERR_STREAM_*` code comes from the stream layer above the socket, so neither is a peer
14
+ * reset and a test that swallows one of those is hiding something real.
15
+ */
16
+ export declare function isPeerReset(err: unknown): boolean;
1
17
  declare const _default: () => Promise<void>;
2
18
  export default _default;
@@ -12,6 +12,8 @@ export declare class Server extends EventEmitter {
12
12
  allowHalfOpen: boolean;
13
13
  private _service;
14
14
  private _connections;
15
+ /** A listener `close()` stopped whose descriptors are not closed yet. */
16
+ private _closingService;
15
17
  private _address;
16
18
  constructor(connectionListener?: (socket: Socket) => void);
17
19
  constructor(options?: {
@@ -31,8 +33,15 @@ export declare class Server extends EventEmitter {
31
33
  family: string;
32
34
  address: string;
33
35
  } | null;
34
- /** Close the server, stop accepting new connections. */
36
+ /**
37
+ * Stop accepting. Node's contract (net.js `Server.prototype.close`):
38
+ * connections already accepted stay open, 'close' follows once the listener
39
+ * AND the last of them are closed, and on a server that is not listening the
40
+ * callback gets ERR_SERVER_NOT_RUNNING — no 'error' event.
41
+ */
35
42
  close(callback?: (err?: Error) => void): this;
43
+ /** Node's `_emitCloseIfDrained`: nothing listening, closing, or connected. */
44
+ private _emitCloseIfDrained;
36
45
  /** Get the number of concurrent connections. */
37
46
  getConnections(callback: (err: Error | null, count: number) => void): void;
38
47
  ref(): this;
@@ -1,5 +1,6 @@
1
1
  import Gio from '@girs/gio-2.0';
2
2
  import { Duplex } from 'node:stream';
3
+ import { Buffer } from 'node:buffer';
3
4
  import type { DuplexOptions } from 'node:stream';
4
5
  export interface SocketConnectOptions {
5
6
  port: number;
@@ -33,7 +34,36 @@ export declare class Socket extends Duplex {
33
34
  private _inputStream;
34
35
  private _outputStream;
35
36
  private _cancellable;
37
+ /**
38
+ * Cancellable dedicated to the read loop's `read_bytes_async` calls,
39
+ * separate from `_cancellable` (which also guards writes). `_destroy`
40
+ * cancels both; `_detachReader` cancels only this one, so handing this
41
+ * socket's connection off to a new owner (a TLS upgrade) never aborts
42
+ * an unrelated in-flight write.
43
+ */
44
+ private _readCancellable;
36
45
  private _reading;
46
+ /**
47
+ * Set by `_detachReader()` while waiting for the current read to settle.
48
+ * The read loop checks this instead of pushing to the Duplex, so a read
49
+ * that resolves with real data (won the race against cancellation) goes
50
+ * to the detacher instead of being delivered on a socket about to be
51
+ * abandoned.
52
+ */
53
+ private _detaching;
54
+ private _detachResolve;
55
+ /**
56
+ * Gio operations started on this socket's streams whose callback has not run.
57
+ * Each one keeps a GSource polling the descriptor; `_destroy` cancels them, but
58
+ * a cancelled operation is only dispatched on the NEXT main-loop iteration, so
59
+ * closing the descriptor before then leaves a source polling a closed fd. On
60
+ * Linux poll(2) flags that entry POLLNVAL and moves on; GLib on darwin polls
61
+ * via select(2), which fails the WHOLE call with EBADF ("poll(2) failed due
62
+ * to: Bad file descriptor") and starves every other source for that
63
+ * iteration. So the close waits for the count to reach zero.
64
+ */
65
+ private _pendingIo;
66
+ private _afterIo;
37
67
  private _timeout;
38
68
  private _timeoutId;
39
69
  constructor(options?: SocketOptions);
@@ -56,18 +86,75 @@ export declare class Socket extends Duplex {
56
86
  * Nullifies the socket's stream references so they are not closed when the
57
87
  * socket itself is later destroyed. */
58
88
  _releaseIOStream(): Gio.IOStream | null;
89
+ /**
90
+ * @internal Atomically hand off this socket's Gio connection/streams to
91
+ * a new owner (a protocol upgrade: TLS, …), nulling them out here.
92
+ * Unlike `_releaseIOStream` (which only ever sees a stolen IOStream,
93
+ * never mid-read — see its one call site), this is also safe to call
94
+ * on a socket whose read loop is already running: nulling
95
+ * `_inputStream` makes both `_startReading()`'s own guard and a
96
+ * `Server._handleConnection`-deferred auto-start no-op, but an
97
+ * ALREADY in-flight `read_bytes_async` keeps its own local stream
98
+ * reference regardless — pair this with `_detachReader()` (either
99
+ * order) when a read may already be pending.
100
+ */
101
+ _claimConnection(): {
102
+ connection: Gio.SocketConnection | null;
103
+ ioStream: Gio.IOStream | null;
104
+ inputStream: Gio.InputStream | null;
105
+ outputStream: Gio.OutputStream | null;
106
+ };
59
107
  /**
60
108
  * Initiate a TCP connection.
61
109
  */
62
110
  connect(options: SocketConnectOptions | number, host?: string | (() => void), connectionListener?: () => void): this;
63
- /** @internal Set up streams and emit connect after connection is established. */
64
- _setupConnection(opts: SocketConnectOptions | Record<string, never>): void;
65
- private _startReading;
111
+ /**
112
+ * @internal Set up streams and emit connect after connection is
113
+ * established. `autoStartReading` (default true) controls the
114
+ * trailing `_startReading()` call — `Server._handleConnection` passes
115
+ * `false` so a synchronous 'connection' listener (e.g.
116
+ * `TLSServer._upgradeTls`) gets a chance to claim the streams first
117
+ * via `_claimConnection()`, then calls `_startReading()` itself once
118
+ * every listener has run. `Socket.connect()`'s own 'connect' emit
119
+ * already runs BEFORE this method's trailing auto-start either way —
120
+ * a claiming listener there needs no such flag, just to call
121
+ * `_claimConnection()` synchronously within its own handler.
122
+ */
123
+ _setupConnection(opts: SocketConnectOptions | Record<string, never>, autoStartReading?: boolean): void;
124
+ /**
125
+ * @internal Not TS-`private`: `Server._handleConnection` (same
126
+ * package, different class) calls this directly when it deferred
127
+ * `_setupConnection`'s own auto-start — see that method's doc.
128
+ */
129
+ _startReading(): void;
66
130
  private _readLoop;
131
+ /**
132
+ * @internal Stop the read loop and cancel any in-flight read, then wait
133
+ * for it to settle — without touching the underlying connection or
134
+ * streams. Used before handing this socket's Gio connection to a new
135
+ * owner (a TLS upgrade / STARTTLS): the new owner's
136
+ * `Gio.TlsClientConnection` issues its own reads on the same
137
+ * `Gio.InputStream`, and GIO rejects a second concurrent read on one
138
+ * stream — so any read this socket started must be fully settled first.
139
+ *
140
+ * Resolves `null` in the well-behaved case (nothing was pending, or the
141
+ * pending read was cleanly cancelled before any data arrived — true for
142
+ * any protocol where the peer waits for our next message, which is what
143
+ * makes STARTTLS work at all). Resolves the leftover `Buffer` when a
144
+ * read raced ahead and already had data by the time it noticed the
145
+ * detach; the caller cannot safely feed that back into a
146
+ * `Gio.TlsClientConnection` (unlike Node's OpenSSL binding, Gio's TLS
147
+ * connection owns and reads its base stream directly — there's no
148
+ * "inject already-read bytes" API), so it must treat this as an error.
149
+ */
150
+ _detachReader(): Promise<Buffer | null>;
151
+ private _resolveDetach;
67
152
  _read(_size: number): void;
68
153
  _write(chunk: unknown, encoding: string, callback: (error?: Error | null) => void): void;
69
154
  _final(callback: (error?: Error | null) => void): void;
70
155
  _destroy(err: Error | null, callback: (error?: Error | null) => void): void;
156
+ private _ioSettled;
157
+ private _release;
71
158
  /** Set the socket timeout in milliseconds. 0 disables. */
72
159
  setTimeout(timeout: number, callback?: () => void): this;
73
160
  private _resetTimeout;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/net",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "Node.js net module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -41,18 +41,18 @@
41
41
  "net"
42
42
  ],
43
43
  "devDependencies": {
44
- "@gjsify/cli": "^0.52.0",
45
- "@gjsify/unit": "^0.52.0",
44
+ "@gjsify/cli": "^0.54.0",
45
+ "@gjsify/unit": "^0.54.0",
46
46
  "@types/node": "^25.9.2",
47
47
  "typescript": "^6.0.3"
48
48
  },
49
49
  "dependencies": {
50
50
  "@girs/gio-2.0": "5.4.0",
51
51
  "@girs/glib-2.0": "5.4.0",
52
- "@gjsify/buffer": "^0.52.0",
53
- "@gjsify/events": "^0.52.0",
54
- "@gjsify/stream": "^0.52.0",
55
- "@gjsify/utils": "^0.52.0"
52
+ "@gjsify/buffer": "^0.54.0",
53
+ "@gjsify/events": "^0.54.0",
54
+ "@gjsify/stream": "^0.54.0",
55
+ "@gjsify/utils": "^0.54.0"
56
56
  },
57
57
  "gjsify": {
58
58
  "runtimes": {