@gjsify/http2 0.4.3 → 0.4.4

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.
@@ -1 +1 @@
1
- var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0}),t=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,__esmMin=(e,t)=>()=>(e&&(t=e(e=0)),t),__exportAll=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},__copyProps=(i,a,o,s)=>{if(a&&typeof a==`object`||typeof a==`function`)for(var c=n(a),l=0,u=c.length,d;l<u;l++)d=c[l],!r.call(i,d)&&d!==o&&e(i,d,{get:(e=>a[e]).bind(null,d),enumerable:!(s=t(a,d))||s.enumerable});return i},__toCommonJS=t=>r.call(t,`module.exports`)?t[`module.exports`]:__copyProps(e({},`__esModule`,{value:!0}),t);export{__esmMin,__exportAll,__name,__toCommonJS};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{constants as e,getDefaultSettings as t}from"./protocol.js";import n from"@girs/soup-3.0";import r from"@girs/gio-2.0";import i from"@girs/glib-2.0";import{EventEmitter as a}from"node:events";import{Duplex as o}from"node:stream";import{Buffer as s}from"node:buffer";import{readBytesAsync as c}from"@gjsify/utils";var Http2Session=class extends a{type=e.NGHTTP2_SESSION_CLIENT;alpnProtocol=void 0;encrypted=!1;_closed=!1;_destroyed=!1;_settings;constructor(){super(),this._settings=t()}get closed(){return this._closed}get destroyed(){return this._destroyed}get connecting(){return!1}get pendingSettingsAck(){return!1}get localSettings(){return{...this._settings}}get remoteSettings(){return t()}get originSet(){return new Set}settings(e,t){Object.assign(this._settings,e),t&&Promise.resolve().then(t)}goaway(t,n,r){this.emit(`goaway`,t??e.NGHTTP2_NO_ERROR),this.destroy()}ping(e,t){let n=e||new Uint8Array(8);return t&&Promise.resolve().then(()=>t(null,0,n)),!0}close(e){this._closed||(this._closed=!0,this.emit(`close`),e&&e())}destroy(e,t){this._destroyed||(this._destroyed=!0,this._closed=!0,e&&this.emit(`error`,e),t!==void 0&&this.emit(`goaway`,t),this.emit(`close`))}ref(){}unref(){}},ClientHttp2Stream=class extends o{id=1;pending=!1;aborted=!1;bufferSize=0;endAfterHeaders=!1;_session;_requestHeaders;_requestChunks=[];_cancellable;_state=e.NGHTTP2_STREAM_STATE_OPEN;_responseHeaders={};get state(){return this._state}get rstCode(){return e.NGHTTP2_NO_ERROR}get session(){return this._session}get sentHeaders(){return this._requestHeaders}constructor(e,t){super(),this._session=e,this._requestHeaders=t,this._cancellable=new r.Cancellable}_read(e){}_write(e,t,n){let r=s.isBuffer(e)?e:s.from(e,t);this._requestChunks.push(r),n()}_final(e){this._sendRequest().then(()=>e()).catch(t=>e(t instanceof Error?t:Error(String(t))))}async _sendRequest(){let t=this._session._getSoupSession(),r=this._session._getAuthority(),a=this._requestHeaders[`:method`]||`GET`,o=this._requestHeaders[`:path`]||`/`,l=`${this._requestHeaders[`:scheme`]||(r.startsWith(`https`)?`https`:`http`)}://${this._requestHeaders[`:authority`]||r.replace(/^https?:\/\//,``)}${o}`,u;try{u=i.Uri.parse(l,i.UriFlags.NONE)}catch{throw Error(`Invalid HTTP/2 request URL: ${l}`)}let d=new n.Message({method:a,uri:u}),f=d.get_request_headers();for(let[e,t]of Object.entries(this._requestHeaders))if(!e.startsWith(`:`))if(Array.isArray(t))for(let n of t)f.append(e,n);else f.replace(e,t);let p=s.concat(this._requestChunks);if(p.length>0){let e=this._requestHeaders[`content-type`]||`application/octet-stream`;d.set_request_body_from_bytes(e,new i.Bytes(p))}try{let n=await new Promise((e,n)=>{t.send_async(d,i.PRIORITY_DEFAULT,this._cancellable,(r,i)=>{try{e(t.send_finish(i))}catch(e){n(e)}})}),r=d.status_code,a=d.get_response_headers(),o={":status":String(r)};a.foreach((e,t)=>{let n=e.toLowerCase();if(n in o){let e=o[n];Array.isArray(e)?e.push(t):o[n]=[e,t]}else o[n]=t}),this._responseHeaders=o,this._state=e.NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL,this.emit(`response`,o,0);try{let e;for(;(e=await c(n,16384,i.PRIORITY_DEFAULT,this._cancellable))!==null;)e.length>0&&this.push(s.from(e))}catch{}this.push(null),this._state=e.NGHTTP2_STREAM_STATE_CLOSED}catch(t){this._state=e.NGHTTP2_STREAM_STATE_CLOSED,this._cancellable.is_cancelled()||this.destroy(t instanceof Error?t:Error(String(t)))}}close(t,n){this._cancellable.cancel(),this._state=e.NGHTTP2_STREAM_STATE_CLOSED,this.emit(`close`,t??e.NGHTTP2_NO_ERROR),n&&n()}priority(e){}sendTrailers(e){}setTimeout(e,t){return this}},ClientHttp2Session=class extends Http2Session{type=e.NGHTTP2_SESSION_CLIENT;_authority;_soupSession;_streams=new Set;constructor(e,t={}){super(),this._authority=e,this.encrypted=e.startsWith(`https:`),this._soupSession=new n.Session,t.rejectUnauthorized===!1&&this._soupSession.connect(`accept-certificate`,(e,t,n)=>!0),Promise.resolve().then(()=>{this.destroyed||(this.alpnProtocol=this.encrypted?`h2`:void 0,this.emit(`connect`,this,null))})}_getSoupSession(){return this._soupSession}_getAuthority(){return this._authority}request(e,t){if(this.destroyed||this.closed)throw Error(`Session is closed`);let n={...e};n[`:scheme`]||=this.encrypted?`https`:`http`,n[`:authority`]||=this._authority.replace(/^https?:\/\//,``),n[`:method`]||=`GET`,n[`:path`]||=`/`;let r=new ClientHttp2Stream(this,n);return this._streams.add(r),r.once(`close`,()=>this._streams.delete(r)),t?.endStream&&r.end(),r}close(e){for(let e of this._streams)e.close();this._streams.clear(),super.close(e)}destroy(e,t){for(let e of this._streams)e.close();this._streams.clear(),super.destroy(e,t)}};export{ClientHttp2Session,ClientHttp2Stream,Http2Session};
1
+ import{__toCommonJS as e}from"./_virtual/_rolldown/runtime.js";import{constants as t,getDefaultSettings as n}from"./protocol.js";import{init_native_client_dispatcher as r,native_client_dispatcher_exports as i}from"./native-client-dispatcher.js";import a from"@girs/soup-3.0";import o from"@girs/gio-2.0";import s from"@girs/glib-2.0";import{EventEmitter as c}from"node:events";import{Duplex as l}from"node:stream";import{Buffer as u}from"node:buffer";import{readBytesAsync as d}from"@gjsify/utils";var Http2Session=class extends c{type=t.NGHTTP2_SESSION_CLIENT;alpnProtocol=void 0;encrypted=!1;_closed=!1;_destroyed=!1;_settings;constructor(){super(),this._settings=n()}get closed(){return this._closed}get destroyed(){return this._destroyed}get connecting(){return!1}get pendingSettingsAck(){return!1}get localSettings(){return{...this._settings}}get remoteSettings(){return n()}get originSet(){return new Set}settings(e,t){Object.assign(this._settings,e),t&&Promise.resolve().then(t)}goaway(e,n,r){this.emit(`goaway`,e??t.NGHTTP2_NO_ERROR),this.destroy()}ping(e,t){let n=e||new Uint8Array(8);return t&&Promise.resolve().then(()=>t(null,0,n)),!0}close(e){this._closed||(this._closed=!0,this.emit(`close`),e&&e())}destroy(e,t){this._destroyed||(this._destroyed=!0,this._closed=!0,e&&this.emit(`error`,e),t!==void 0&&this.emit(`goaway`,t),this.emit(`close`))}ref(){}unref(){}},ClientHttp2Stream=class extends l{_id=1;pending=!1;aborted=!1;bufferSize=0;endAfterHeaders=!1;_session;_requestHeaders;_requestChunks=[];_cancellable;_state=t.NGHTTP2_STREAM_STATE_OPEN;_responseHeaders={};_nativeStreamId=0;get id(){return this._id}get state(){return this._state}get rstCode(){return t.NGHTTP2_NO_ERROR}get session(){return this._session}get sentHeaders(){return this._requestHeaders}_setNativeStreamId(e){this._id=e,this._nativeStreamId=e,this._wireNativeBody()}constructor(e,t){super(),this._session=e,this._requestHeaders=t,this._cancellable=new o.Cancellable}_read(e){}_write(e,t,n){let r=u.isBuffer(e)?e:u.from(e,t);this._nativeStreamId>0?this._session._getNativeClient().writeData(this._nativeStreamId,r,!1):this._requestChunks.push(r),n()}_final(e){let t=this._session._getNativeClient();if(t){try{if(this._nativeStreamId>0)t.writeData(this._nativeStreamId,u.alloc(0),!0);else{let n=u.concat(this._requestChunks),r=n.length===0,i=t.submitRequest(this._requestHeaders,r);if(i===0){e(Error(`Failed to submit HTTP/2 request stream`));return}this._id=i,this._nativeStreamId=i,n.length>0&&t.writeData(i,n,!0),this._wireNativeBody()}e()}catch(t){e(t instanceof Error?t:Error(String(t)))}return}this._sendRequest().then(()=>e()).catch(t=>e(t instanceof Error?t:Error(String(t))))}_wireNativeBody(){let e=this._session._getNativeClient();if(!e||this._nativeStreamId===0)return;let n=this._nativeStreamId;(async()=>{try{let r=Date.now()+3e4,i=null;for(;Date.now()<r&&(i=e.responseHeaders(n),!i);)await new Promise(e=>s.idle_add(s.PRIORITY_DEFAULT,()=>(e(),!1)));if(!i){this.destroy(Error(`Native HTTP/2 client: response headers timeout`));return}this._responseHeaders=i,this._state=t.NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL,this.emit(`response`,i,0);for await(let t of e.body(n))t.length>0&&this.push(t);this.push(null),this._state=t.NGHTTP2_STREAM_STATE_CLOSED}catch(e){this._state=t.NGHTTP2_STREAM_STATE_CLOSED,this.destroy(e instanceof Error?e:Error(String(e)))}})()}async _sendRequest(){let e=this._session._getSoupSession(),n=this._session._getAuthority(),r=this._requestHeaders[`:method`]||`GET`,i=this._requestHeaders[`:path`]||`/`,o=`${this._requestHeaders[`:scheme`]||(n.startsWith(`https`)?`https`:`http`)}://${this._requestHeaders[`:authority`]||n.replace(/^https?:\/\//,``)}${i}`,c;try{c=s.Uri.parse(o,s.UriFlags.NONE)}catch{throw Error(`Invalid HTTP/2 request URL: ${o}`)}let l=new a.Message({method:r,uri:c}),f=l.get_request_headers();for(let[e,t]of Object.entries(this._requestHeaders))if(!e.startsWith(`:`))if(Array.isArray(t))for(let n of t)f.append(e,n);else f.replace(e,t);let p=u.concat(this._requestChunks);if(p.length>0){let e=this._requestHeaders[`content-type`]||`application/octet-stream`;l.set_request_body_from_bytes(e,new s.Bytes(p))}try{let n=await new Promise((t,n)=>{e.send_async(l,s.PRIORITY_DEFAULT,this._cancellable,(r,i)=>{try{t(e.send_finish(i))}catch(e){n(e)}})}),r=l.status_code,i=l.get_response_headers(),a={":status":String(r)};i.foreach((e,t)=>{let n=e.toLowerCase();if(n in a){let e=a[n];Array.isArray(e)?e.push(t):a[n]=[e,t]}else a[n]=t}),this._responseHeaders=a,this._state=t.NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL,this.emit(`response`,a,0);try{let e;for(;(e=await d(n,16384,s.PRIORITY_DEFAULT,this._cancellable))!==null;)e.length>0&&this.push(u.from(e))}catch{}this.push(null),this._state=t.NGHTTP2_STREAM_STATE_CLOSED}catch(e){this._state=t.NGHTTP2_STREAM_STATE_CLOSED,this._cancellable.is_cancelled()||this.destroy(e instanceof Error?e:Error(String(e)))}}close(e,n){this._cancellable.cancel(),this._state=t.NGHTTP2_STREAM_STATE_CLOSED,this.emit(`close`,e??t.NGHTTP2_NO_ERROR),n&&n()}priority(e){}sendTrailers(e){}setTimeout(e,t){return this}},ClientHttp2Session=class extends Http2Session{type=t.NGHTTP2_SESSION_CLIENT;_authority;_soupSession;_streams=new Set;_nativeClient=null;get nativeClient(){return this._nativeClient}constructor(e,t={}){super(),this._authority=e,this.encrypted=e.startsWith(`https:`),this._soupSession=new a.Session,t.rejectUnauthorized===!1&&this._soupSession.connect(`accept-certificate`,(e,t,n)=>!0);let n=t.nativeDispatcher??`auto`;n===`force`&&this._setupNativeClient(e,n),Promise.resolve().then(()=>{this.destroyed||(this.alpnProtocol=this.encrypted?`h2`:this._nativeClient?`h2c`:void 0,this.emit(`connect`,this,null))})}_setupNativeClient(t,n){let{Http2NativeClientDispatcher:a}=(r(),e(i));if(!a.available()){if(n===`force`)throw Error(`@gjsify/http2-native prebuild not loadable — nativeDispatcher: "force" cannot proceed`);return}let[o,s]=t.replace(/^https?:\/\//,``).split(`:`),c=parseInt(s||(this.encrypted?`443`:`80`),10);this._nativeClient=new a({onPushPromise:e=>{let t=new ClientHttp2Stream(this,e.headers);t._setNativeStreamId(e.promisedStreamId),this._streams.add(t),this.emit(`stream`,t,e.headers,0)},onGoaway:(e,t)=>{this.emit(`goaway`,t,e)},onClose:()=>{}});try{this._nativeClient.connect(o||`localhost`,c)}catch(e){if(this._nativeClient=null,n===`force`)throw e}}_getSoupSession(){return this._soupSession}_getNativeClient(){return this._nativeClient}_getAuthority(){return this._authority}request(e,t){if(this.destroyed||this.closed)throw Error(`Session is closed`);let n={},r={};for(let[t,i]of Object.entries(e))t.startsWith(`:`)?n[t]=i:r[t]=i;n[`:method`]||=`GET`,n[`:scheme`]||=this.encrypted?`https`:`http`,n[`:authority`]||=this._authority.replace(/^https?:\/\//,``),n[`:path`]||=`/`;let i={...n,...r},a=new ClientHttp2Stream(this,i);return this._streams.add(a),a.once(`close`,()=>this._streams.delete(a)),t?.endStream&&a.end(),a}close(e){for(let e of this._streams)e.close();this._streams.clear(),super.close(e)}destroy(e,t){for(let e of this._streams)e.close();this._streams.clear(),super.destroy(e,t)}};export{ClientHttp2Session,ClientHttp2Stream,Http2Session};
@@ -0,0 +1 @@
1
+ import{__esmMin as e,__exportAll as t}from"./_virtual/_rolldown/runtime.js";import n from"gi://GLib?version=2.0";import r from"gi://Gio?version=2.0";import{hasNativeHttp2 as i,loadNativeHttp2 as a}from"@gjsify/http2-native";var o=t({Http2NativeClientDispatcher:()=>Http2NativeClientDispatcher});function makeBodyIterator(e){return{[Symbol.asyncIterator](){return{next(){return e.bodyBuffer.length>0?Promise.resolve({value:e.bodyBuffer.shift(),done:!1}):e.bodyClosed?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.bodyResolvers.push(t)})}}}}}function headersRecordToArrays(e){let t=[],n=[];for(let[r,i]of Object.entries(e))if(Array.isArray(i))for(let e of i)t.push(r),n.push(String(e));else t.push(r),n.push(String(i));return{names:t,values:n}}var s,Http2NativeClientDispatcher,c=e(()=>{s=16*1024,Http2NativeClientDispatcher=class{_bridge=null;_connection=null;_inSource=null;_streams=new Map;_closed=!1;_callbacks;_connectError=null;constructor(e={}){this._callbacks=e}static available(){return i()}connect(e,t){if(!i())throw Error(`@gjsify/http2-native prebuild not loadable — native client unavailable`);let o=a();if(!o)throw Error(`@gjsify/http2-native load failed`);let c=o.SessionBridge.new_client();if(!c)throw Error(`SessionBridge.new_client() returned null`);let l=new r.SocketClient,u;try{u=l.connect_to_host(e,t,null)}catch(e){throw this._connectError=e instanceof Error?e:Error(String(e)),c.close(),this._connectError}u.get_socket().set_blocking(!1),this._bridge=c,this._connection=u,c.connect(`headers-received`,(e,t,n,r)=>{this._onHeaders(t,n,r)}),c.connect(`data-received`,(e,t,n,r)=>{this._onData(t,n,r)}),c.connect(`stream-closed`,(e,t,n)=>{this._onStreamClosed(t)}),c.connect(`frame-send-ready`,()=>{this._flushOutput()}),c.connect(`goaway-received`,(e,t,n)=>{this._callbacks.onGoaway&&this._callbacks.onGoaway(t,n),this._close()}),c.connect(`push-promise-received`,(e,t,n,r)=>{this._onPushPromise(t,n,r)}),this._flushOutput();let d=u.get_input_stream().create_source(null),cb=()=>{if(this._closed)return!1;try{let e=u.get_input_stream().read_bytes(s,null);if(e.get_size()===0)return this._close(),!1;c.feed_input(e),c.dispatch_pending(),this._flushOutput()}catch(e){return e instanceof n.Error&&e.matches(r.io_error_quark(),r.IOErrorEnum.WOULD_BLOCK)?!0:(this._close(),!1)}return!0};d.set_callback(cb),d.attach(n.MainContext.default()),this._inSource=d}submitRequest(e,t){if(!this._bridge)throw Error(`Native client session is not connected`);let{names:n,values:r}=headersRecordToArrays(e),i=this._bridge.submit_request(n,r,t);return i?(this._streams.set(i,{endStreamFromPeer:!1,responseHeaders:null,bodyBuffer:[],bodyResolvers:[],bodyClosed:!1}),this._flushOutput(),i):0}writeData(e,t,r){if(!this._bridge)return;let i=n.Bytes.new(new Uint8Array(t.buffer,t.byteOffset,t.byteLength));this._bridge.submit_data(e,i,r),this._flushOutput()}body(e){let t=this._streams.get(e);return t?makeBodyIterator(t):(async function*(){})()}responseHeaders(e){let t=this._streams.get(e);return t?t.responseHeaders:null}close(){this._close()}_close(){if(!this._closed){this._closed=!0;try{this._bridge?.submit_goaway(0,0),this._flushOutput()}catch{}if(this._inSource){try{this._inSource.destroy()}catch{}this._inSource=null}try{this._connection?.close(null)}catch{}this._connection=null;try{this._bridge?.close()}catch{}this._bridge=null;for(let e of this._streams.values())for(e.bodyClosed=!0;e.bodyResolvers.length>0;)e.bodyResolvers.shift()({value:void 0,done:!0});this._callbacks.onClose&&this._callbacks.onClose()}}_flushOutput(){if(this._closed||!this._bridge||!this._connection)return;let e=this._bridge.drain_output();if(e.get_size()===0)return;let t=e.get_data();try{this._connection.get_output_stream().write(t,null)}catch{this._close()}}_onHeaders(e,t,n){let r=t.deep_unpack(),i={};for(let[e,t]of r)if(e in i){let n=i[e];Array.isArray(n)?n.push(t):i[e]=[n,t]}else i[e]=t;let a=this._streams.get(e);if(a&&(a.responseHeaders=i,n&&(a.bodyClosed=!0)),this._callbacks.onResponse){let t=a??this._registerStreamForPush(e);this._callbacks.onResponse({streamId:e,headers:i,endStream:n,body:makeBodyIterator(t)})}}_onData(e,t,n){let r=this._streams.get(e);if(r){if(t.get_size()>0){let e=Buffer.from(t.get_data());r.bodyResolvers.length>0?r.bodyResolvers.shift()({value:e,done:!1}):r.bodyBuffer.push(e)}if(n)for(r.bodyClosed=!0;r.bodyResolvers.length>0;)r.bodyResolvers.shift()({value:void 0,done:!0})}}_onStreamClosed(e){let t=this._streams.get(e);if(t)for(t.bodyClosed=!0;t.bodyResolvers.length>0;)t.bodyResolvers.shift()({value:void 0,done:!0})}_onPushPromise(e,t,n){let r=n.deep_unpack(),i={};for(let[e,t]of r)i[e]=t;this._registerStreamForPush(t),this._callbacks.onPushPromise&&this._callbacks.onPushPromise({parentStreamId:e,promisedStreamId:t,headers:i})}_registerStreamForPush(e){let t=this._streams.get(e);return t||(t={endStreamFromPeer:!1,responseHeaders:null,bodyBuffer:[],bodyResolvers:[],bodyClosed:!1},this._streams.set(e,t)),t}}});c();export{Http2NativeClientDispatcher,c as init_native_client_dispatcher,o as native_client_dispatcher_exports};
@@ -0,0 +1 @@
1
+ import{__esmMin as e,__exportAll as t}from"./_virtual/_rolldown/runtime.js";import n from"gi://GLib?version=2.0";import r from"gi://Gio?version=2.0";import{hasNativeHttp2 as i,loadNativeHttp2 as a}from"@gjsify/http2-native";var o=t({Http2NativeDispatcher:()=>Http2NativeDispatcher});function bytesAreH2Preface(e){if(e.length<s)return!1;for(let t=0;t<s;t++)if(e[t]!==l[t])return!1;return!0}function headersRecordToArrays(e){let t=[],n=[];for(let[r,i]of Object.entries(e))if(Array.isArray(i))for(let e of i)t.push(r),n.push(String(e));else t.push(r),n.push(String(i));return{names:t,values:n}}var s,c,Http2NativeDispatcher,l,u=e(()=>{s=24,c=16*1024,Http2NativeDispatcher=class{_service=null;_connections=new Set;_listenPort=0;_handler;_prefaceGate;_onNonH2c;constructor(e){this._handler=e.handler,this._prefaceGate=e.prefaceGate??!1,this._onNonH2c=e.onNonH2c??null}static available(){return i()}listen(e){if(!i())throw Error(`@gjsify/http2-native prebuild not loadable — dispatcher unavailable`);this._service=new r.SocketService;let t=e;if(e>0){if(!this._service.add_inet_port(e,null))throw Error(`Failed to bind native HTTP/2 dispatcher on port ${e}`)}else{let e=r.InetAddress.new_from_string(`0.0.0.0`)??r.InetAddress.new_any(r.SocketFamily.IPV4),n=r.InetSocketAddress.new(e,0),[i,a]=this._service.add_address(n,r.SocketType.STREAM,r.SocketProtocol.TCP,null);if(!i)throw Error(`Failed to bind native HTTP/2 dispatcher on ephemeral port`);let o=a;o&&typeof o.get_port==`function`&&(t=o.get_port())}return this._listenPort=t,this._service.connect(`incoming`,(e,t)=>(this._acceptConnection(t),!0)),this._service.start(),t}close(){if(this._service){this._service.stop();try{this._service.close()}catch{}this._service=null}for(let e of this._connections)this._closeConnection(e);this._connections.clear()}_acceptConnection(e){let t=e.get_socket();t.set_blocking(!1);let r=e.get_remote_address(),i=r?r.get_address().to_string():`127.0.0.1`,a=r&&typeof r.get_port==`function`?r.get_port():0;if(this._prefaceGate)try{if(t.condition_check(n.IOCondition.IN)&n.IOCondition.IN){let[n,r]=t.receive(c,null);if(n>=s&&bytesAreH2Preface(r)){this._setupNativeConnection(e,t,r.slice(0,n),i,a);return}this._onNonH2c&&this._onNonH2c(e,r.slice(0,n));return}}catch{}this._setupNativeConnection(e,t,null,i,a)}_setupNativeConnection(e,t,i,o,s){let l=a();if(!l){try{e.close(null)}catch{}return}let u=l.SessionBridge.new_server();if(!u){try{e.close(null)}catch{}return}let d={bridge:u,socket:t,inStream:e.get_input_stream(),outStream:e.get_output_stream(),inSource:null,streams:new Map,pendingWrite:null,closed:!1,remoteAddress:o,remotePort:s};this._connections.add(d),u.connect(`headers-received`,(e,t,n,r)=>{this._onHeaders(d,t,n,r)}),u.connect(`data-received`,(e,t,n,r)=>{this._onData(d,t,n,r)}),u.connect(`stream-closed`,(e,t,n)=>{this._onStreamClosed(d,t,n)}),u.connect(`frame-send-ready`,()=>{this._flushOutput(d)}),u.connect(`goaway-received`,()=>{this._closeConnection(d)}),i&&i.byteLength>0&&(u.feed_input(n.Bytes.new(i)),u.dispatch_pending(),this._flushOutput(d)),this._flushOutput(d);let f=d.inStream.create_source(null),cb=()=>{if(d.closed)return!1;try{let e=d.inStream.read_bytes(c,null);if(e.get_size()===0)return this._closeConnection(d),!1;u.feed_input(e),u.dispatch_pending(),this._flushOutput(d)}catch(e){return e instanceof n.Error&&e.matches(r.io_error_quark(),r.IOErrorEnum.WOULD_BLOCK)?!0:(this._closeConnection(d),!1)}return!0};f.set_callback(cb),f.attach(n.MainContext.default()),d.inSource=f}_flushOutput(e){if(e.closed)return;let t=e.bridge.drain_output();if(t.get_size()===0)return;let n=t.get_data();try{e.outStream.write(n,null)}catch{this._closeConnection(e)}}_onHeaders(e,t,n,r){let i=n.deep_unpack(),a={};for(let[e,t]of i){let n=e.toLowerCase();if(n in a){let e=a[n];Array.isArray(e)?e.push(t):a[n]=[e,t]}else a[n]=t}let o={endStreamFromPeer:r,bodyBuffer:[],bodyResolvers:[],bodyClosed:r,responseSubmitted:!1};e.streams.set(t,o);let s={streamId:t,headers:a,endStream:r,remoteAddress:e.remoteAddress,remotePort:e.remotePort,localPort:this._listenPort,body:this._makeBodyIterator(o),backend:this._makeBackend(e,t)};Promise.resolve().then(()=>{try{this._handler(s)}catch(e){let t=e;console.error(`Http2NativeDispatcher: handler threw:`,t.message??String(e),t.stack??``)}})}_onData(e,t,n,r){let i=e.streams.get(t);if(i){if(n.get_size()>0){let e=Buffer.from(n.get_data());i.bodyResolvers.length>0?i.bodyResolvers.shift()({value:e,done:!1}):i.bodyBuffer.push(e)}if(r)for(i.bodyClosed=!0;i.bodyResolvers.length>0;)i.bodyResolvers.shift()({value:void 0,done:!0})}}_onStreamClosed(e,t,n){let r=e.streams.get(t);if(r)for(r.bodyClosed=!0;r.bodyResolvers.length>0;)r.bodyResolvers.shift()({value:void 0,done:!0});e.streams.delete(t)}_makeBackend(e,t){return{streamId:t,respond:(n,r)=>{this._submitResponse(e,t,n,r)},writeData:(n,r)=>{this._submitData(e,t,n,r)},reset:(n=0)=>{try{e.bridge.submit_rst_stream(t,n)}catch{}this._flushOutput(e)},pushPromise:n=>{let{names:r,values:i}=headersRecordToArrays(n),a=e.bridge.submit_push_promise(t,r,i);return this._flushOutput(e),a?(e.streams.set(a,{endStreamFromPeer:!0,bodyBuffer:[],bodyResolvers:[],bodyClosed:!0,responseSubmitted:!1}),this._makeBackend(e,a)):null}}}_submitResponse(e,t,n,r){let i=e.streams.get(t);if(!i||i.responseSubmitted)return;i.responseSubmitted=!0;let{names:a,values:o}=headersRecordToArrays(n);e.bridge.submit_response(t,a,o,r),this._flushOutput(e)}_submitData(e,t,r,i){if(r.length===0&&!i)return;let a=n.Bytes.new(new Uint8Array(r.buffer,r.byteOffset,r.byteLength));e.bridge.submit_data(t,a,i),this._flushOutput(e)}_makeBodyIterator(e){return{[Symbol.asyncIterator](){return{next(){return e.bodyBuffer.length>0?Promise.resolve({value:e.bodyBuffer.shift(),done:!1}):e.bodyClosed?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.bodyResolvers.push(t)})}}}}}_closeConnection(e){if(e.closed)return;e.closed=!0;try{e.bridge.submit_goaway(0,0)}catch{}try{this._flushOutput(e)}catch{}if(e.inSource){try{e.inSource.destroy()}catch{}e.inSource=null}try{e.socket.shutdown(!1,!0)}catch{}for(let t of e.streams.values())for(t.bodyClosed=!0;t.bodyResolvers.length>0;)t.bodyResolvers.shift()({value:void 0,done:!0});let finalClose=()=>{try{e.outStream.close(null)}catch{}try{e.inStream.close(null)}catch{}try{e.socket.close()}catch{}try{e.bridge.close()}catch{}};n.idle_add(n.PRIORITY_DEFAULT,()=>(finalClose(),!1)),this._connections.delete(e)}},l=new Uint8Array([80,82,73,32,42,32,72,84,84,80,47,50,46,48,13,10,13,10,83,77,13,10,13,10])});u();export{Http2NativeDispatcher,u as init_native_dispatcher,o as native_dispatcher_exports};
package/lib/esm/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import"./_virtual/_rolldown/runtime.js";import{constants as e,getDefaultSettings as t}from"./protocol.js";import n from"@girs/soup-3.0";import r from"@girs/gio-2.0";import i from"@girs/glib-2.0";import{EventEmitter as a}from"node:events";import{Readable as o,Writable as s}from"node:stream";import{Buffer as c}from"node:buffer";import{deferEmit as l,ensureMainLoop as u}from"@gjsify/utils";import{closeSync as d,openSync as f,read as p,statSync as m}from"node:fs";import{loadNativeHttp2 as h}from"@gjsify/http2-native";var Http2ServerRequest=class extends o{method=`GET`;url=`/`;headers={};rawHeaders=[];authority=``;scheme=`https`;httpVersion=`2.0`;httpVersionMajor=2;httpVersionMinor=0;complete=!1;socket=null;trailers={};rawTrailers=[];_stream=null;_timeoutTimer=null;get stream(){return this._stream}_setStream(e){this._stream=e}constructor(){super()}_read(e){}_autoClose(){}_pushBody(e){e&&e.length>0&&this.push(c.from(e)),this.push(null),this.complete=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null)}setTimeout(e,t){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),t&&this.once(`timeout`,t),e>0&&(this._timeoutTimer=setTimeout(()=>{this._timeoutTimer=null,this.emit(`timeout`)},e)),this}destroy(e){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),super.destroy(e)}},Http2ServerResponse=class extends s{statusCode=200;statusMessage=``;headersSent=!1;finished=!1;sendDate=!0;_soupMsg;_headers=new Map;_streaming=!1;_timeoutTimer=null;_stream=null;_detachedBody=null;get stream(){return this._stream}get socket(){return null}get isDetached(){return this._soupMsg===null}get detachedBody(){return this._detachedBody?c.concat(this._detachedBody):null}_setStream(e){this._stream=e}constructor(e){super(),this._soupMsg=e,e===null&&(this._detachedBody=[])}setHeader(e,t){return this._headers.set(e.toLowerCase(),typeof t==`number`?String(t):t),this}getHeader(e){return this._headers.get(e.toLowerCase())}removeHeader(e){this._headers.delete(e.toLowerCase())}hasHeader(e){return this._headers.has(e.toLowerCase())}getHeaderNames(){return Array.from(this._headers.keys())}getHeaders(){let e={};for(let[t,n]of this._headers)e[t]=n;return e}appendHeader(e,t){let n=e.toLowerCase(),r=this._headers.get(n);return r===void 0?this._headers.set(n,t):Array.isArray(r)?Array.isArray(t)?r.push(...t):r.push(t):this._headers.set(n,Array.isArray(t)?[r,...t]:[r,t]),this}flushHeaders(){this.headersSent||=!0}writeHead(e,t,n){if(this.statusCode=e,typeof t==`object`&&(n=t,t=void 0),typeof t==`string`&&(this.statusMessage=t),n)for(let[e,t]of Object.entries(n))this.setHeader(e,t);return this}respond(e,t){let n=Number(e[`:status`]??200),r={};for(let[t,n]of Object.entries(e))t!==`:status`&&(r[t]=typeof n==`number`?String(n):n);this.writeHead(n,r),t?.endStream&&this.end()}writeContinue(e){e&&Promise.resolve().then(e)}writeEarlyHints(e,t){t&&Promise.resolve().then(t)}addTrailers(e){}setTimeout(e,t){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),t&&this.once(`timeout`,t),e>0&&(this._timeoutTimer=setTimeout(()=>{this._timeoutTimer=null,this.emit(`timeout`)},e)),this}_startStreaming(){if(this._streaming||(this._streaming=!0,this.headersSent=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),!this._soupMsg))return;this._soupMsg.set_status(this.statusCode,this.statusMessage||null);let e=this._soupMsg.get_response_headers();this._headers.has(`content-length`)?e.set_encoding(n.Encoding.CONTENT_LENGTH):e.set_encoding(n.Encoding.CHUNKED);for(let[t,n]of this._headers)if(Array.isArray(n))for(let r of n)e.append(t,r);else e.replace(t,n)}_write(e,t,n){let r=c.isBuffer(e)?e:c.from(e,t);this._startStreaming(),this._soupMsg?(this._soupMsg.get_response_body().append(new Uint8Array(r.buffer,r.byteOffset,r.byteLength)),this._soupMsg.unpause()):this._detachedBody&&this._detachedBody.push(r),n()}_final(e){this._streaming?this._soupMsg&&(this._soupMsg.get_response_body().complete(),this._soupMsg.unpause()):this._sendBatchResponse(),this.finished=!0,e()}_sendBatchResponse(){if(this.headersSent||(this.headersSent=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),!this._soupMsg))return;this._soupMsg.set_status(this.statusCode,this.statusMessage||null);let e=this._soupMsg.get_response_headers();for(let[t,n]of this._headers)if(Array.isArray(n))for(let r of n)e.append(t,r);else e.replace(t,n);let t=this._headers.get(`content-type`)||`text/plain`;this._soupMsg.set_response(t,n.MemoryUse.COPY,new Uint8Array)}end(e,t,n){return typeof e==`function`?(n=e,e=void 0):typeof t==`function`&&(n=t,t=void 0),e!=null&&this.write(e,t),super.end(n),this}respondWithFD(e,t,n){_respondFromFD(this,e,t,n??{},!1)}respondWithFile(e,t,n){let r;try{r=f(e,`r`)}catch(e){if(n?.onError){n.onError(e);return}throw e}_respondFromFD(this,r,t,n??{},!0)}pushStream(e,t,n){if(typeof t==`function`&&(n=t,t={}),!n)throw TypeError(`callback must be a function`);if(!this._stream){n(Error(`No associated stream`),null,{});return}this._stream.pushStream(e,t,n)}createPushResponse(e,t){if(typeof t!=`function`)throw TypeError(`callback must be a function`);this.pushStream(e,{},(e,n)=>{if(e){t(e,null);return}let r=n._res;t(null,r??null)})}},g=class ServerHttp2Stream extends a{id;pushAllowed;sentHeaders={};_res;_session;_isPushedStream;_pushedChildren=[];_pushPromiseFrame=null;_pushRequestHeaders=null;get session(){return this._session}get headersSent(){return this._res.headersSent}get closed(){return this._res.writableEnded}get destroyed(){return this._res.destroyed}get pending(){return!1}get state(){return this.closed?e.NGHTTP2_STREAM_STATE_CLOSED:e.NGHTTP2_STREAM_STATE_OPEN}get pushPromiseFrame(){return this._pushPromiseFrame}get pushRequestHeaders(){return this._pushRequestHeaders}get pushedChildren(){return this._pushedChildren}constructor(e,t=null,n={}){super(),this._res=e,this._session=t,this._isPushedStream=n.isPushedStream===!0,this.id=n.streamId??1,this.pushAllowed=!this._isPushedStream&&t?.canPush!==!1,e.on(`finish`,()=>this.emit(`close`)),e.on(`error`,e=>this.emit(`error`,e))}respond(e,t){this._res.respond(e,t)}write(e,t,n){return this._res.write(e,t,n)}end(e,t,n){return this._res.end(e,t,n),this}destroy(e){return this._res.destroy(e),this}close(e,t){t&&this.once(`close`,t),this._res.end()}priority(e){}setTimeout(e,t){return this._res.setTimeout(e,t),this}sendTrailers(e){}additionalHeaders(e){}respondWithFD(e,t,n){this._res.respondWithFD(e,t,n)}respondWithFile(e,t,n){this._res.respondWithFile(e,t,n)}pushStream(e,t,n){if(typeof t==`function`&&(n=t,t={}),!n)throw TypeError(`callback must be a function`);if(this._isPushedStream){let e=Object.assign(Error(`Cannot initiate nested push streams`),{code:`ERR_HTTP2_NESTED_PUSH`});n(e,null,{});return}if(this._session&&this._session.canPush===!1){let e=Object.assign(Error(`HTTP/2 server push has been disabled`),{code:`ERR_HTTP2_PUSH_DISABLED`});n(e,null,{});return}let r,i=null,a={},o={};for(let[t,n]of Object.entries(e))o[t]=typeof n==`number`?String(n):n;if(o[`:method`]||=`GET`,a=o,this._session){if(r=this._session._allocatePushId(),r===0){let e=Object.assign(Error(`No available stream ids`),{code:`ERR_HTTP2_OUT_OF_STREAMS`});n(e,null,{});return}i=this._session._buildPushPromise(this.id,r,o)}else r=2;let s=new Http2ServerResponse(_makeDetachedSoupMessage()),c=new ServerHttp2Stream(s,this._session,{isPushedStream:!0,streamId:r});c._pushPromiseFrame=i,c._pushRequestHeaders=o,s._setStream(c),this._pushedChildren.push(c),Promise.resolve().then(()=>{n(null,c,a)})}},ServerHttp2Session=class extends a{type=e.NGHTTP2_SESSION_SERVER;alpnProtocol=`h2`;encrypted=!0;_closed=!1;_destroyed=!1;_settings;_canPush=!0;_frameEncoder=null;_streamIdAllocator=null;_fallbackPushId=2;constructor(){super(),this._settings=t()}get canPush(){return this._canPush}set canPush(e){this._canPush=e}_allocatePushId(){let e=h();if(e)return this._streamIdAllocator||=e.StreamIdAllocator.new(),this._streamIdAllocator.next_promised();let t=this._fallbackPushId;return t>2147483647?0:(this._fallbackPushId+=2,t)}_buildPushPromise(e,t,n){let r=h();if(!r)return null;this._frameEncoder||=r.FrameEncoder.new();let i=[],a=[];for(let[e,t]of Object.entries(n)){let n=e.toLowerCase();if(Array.isArray(t))for(let e of t)i.push(n),a.push(String(e));else i.push(n),a.push(String(t))}let o=this._frameEncoder.encode_headers(i,a);if(!o)return null;let s=this._frameEncoder.build_push_promise(e,t,o),c=s.toArray;if(typeof c==`function`)return c.call(s);let l=s.get_data;return typeof l==`function`?l.call(s)??null:null}get closed(){return this._closed}get destroyed(){return this._destroyed}get pendingSettingsAck(){return!1}get localSettings(){return{...this._settings}}get remoteSettings(){return t()}get originSet(){return[]}settings(e,t){Object.assign(this._settings,e),t&&Promise.resolve().then(t)}goaway(t,n,r){this.emit(`goaway`,t??e.NGHTTP2_NO_ERROR),this.destroy()}ping(e,t){let n=new Uint8Array(8);return t&&Promise.resolve().then(()=>t(null,0,n)),!0}close(e){this._closed||(this._closed=!0,this.emit(`close`),e&&e())}destroy(e,t){this._destroyed||(this._destroyed=!0,this._closed=!0,e&&this.emit(`error`,e),t!==void 0&&this.emit(`goaway`,t),this.emit(`close`))}altsvc(e,t){}origin(...e){}ref(){}unref(){}};const _=new Set;var Http2Server=class extends a{listening=!1;maxHeadersCount=2e3;timeout=0;_soupServer=null;_address=null;_options;get soupServer(){return this._soupServer}constructor(e,t){super(),typeof e==`function`&&(t=e,e={}),this._options=e??{},t&&this.on(`request`,t)}listen(...e){let t=0,r=`0.0.0.0`,i;for(let n of e)typeof n==`number`?t=n:typeof n==`string`?r=n:typeof n==`function`&&(i=n);i&&this.once(`listening`,i);try{this._soupServer=new n.Server({}),this._configureSoupServer(this._soupServer),this._soupServer.add_handler(null,(e,t,n)=>{this._handleRequest(t)}),this._soupServer.listen_local(t,n.ServerListenOptions.IPV4_ONLY),u();let e=this._soupServer.get_listeners(),i=t;if(e&&e.length>0){let t=e[0].get_local_address();t&&typeof t.get_port==`function`&&(i=t.get_port())}this.listening=!0,this._address={port:i,family:`IPv4`,address:r},_.add(this),l(this,`listening`)}catch(e){let t=e instanceof Error?e:Error(String(e));if(this.listenerCount(`error`)===0)throw t;l(this,`error`,t)}return this}_configureSoupServer(e){}_handleRequest(e){let t=new Http2ServerRequest,i=new Http2ServerResponse(e);t.method=e.get_method();let a=e.get_uri(),o=a.get_path(),s=a.get_query();t.url=s?o+`?`+s:o,t.authority=a.get_host()??``,t.scheme=a.get_scheme()??`http`,e.get_http_version()===n.HTTPVersion.HTTP_2_0?(t.httpVersion=`2.0`,t.httpVersionMajor=2,t.httpVersionMinor=0):(t.httpVersion=`1.1`,t.httpVersionMajor=1,t.httpVersionMinor=1),e.get_request_headers().foreach((e,n)=>{let r=e.toLowerCase();if(t.rawHeaders.push(e,n),r in t.headers){let e=t.headers[r];Array.isArray(e)?e.push(n):t.headers[r]=[e,n]}else t.headers[r]=n});let c=e.get_remote_host()??`127.0.0.1`,l=e.get_remote_address();t.socket={remoteAddress:c,remotePort:l instanceof r.InetSocketAddress?l.get_port():0,localAddress:this._address?.address??`127.0.0.1`,localPort:this._address?.port??0,encrypted:this instanceof Http2SecureServer};let u=e.get_request_body();u?.data&&u.data.length>0?t._pushBody(u.data):t._pushBody(null);let d={":method":t.method,":path":t.url,":authority":t.authority,":scheme":t.scheme,...t.headers};e.pause(),i.on(`finish`,()=>e.unpause());let f=new g(i,new ServerHttp2Session);t._setStream(f),i._setStream(f),this.emit(`stream`,f,d),this.emit(`request`,t,i)}address(){return this._address}close(e){return e&&this.once(`close`,e),this._soupServer&&=(this._soupServer.disconnect(),null),this.listening=!1,_.delete(this),l(this,`close`),this}setTimeout(e,t){return this.timeout=e,t&&this.on(`timeout`,t),this}},Http2SecureServer=class extends Http2Server{_tlsCert=null;constructor(e,t){if(super(e,t),e.cert&&e.key){let t=_toPemString(e.cert),n=_toPemString(e.key);this._tlsCert=_createTlsCertificate(t,n)}else e.pfx}_configureSoupServer(e){this._tlsCert&&e.set_tls_certificate(this._tlsCert)}setSecureContext(e){if(e.cert&&e.key){let t=_toPemString(e.cert),n=_toPemString(e.key);this._tlsCert=_createTlsCertificate(t,n),this._soupServer&&this._tlsCert&&this._soupServer.set_tls_certificate(this._tlsCert)}}};function _toPemString(e){return Array.isArray(e)?e.map(_toPemString).join(`
2
- `):c.isBuffer(e)?e.toString(`utf8`):e}function _createTlsCertificate(e,t){let n=e.trimEnd()+`
1
+ import{__toCommonJS as e}from"./_virtual/_rolldown/runtime.js";import{constants as t,getDefaultSettings as n}from"./protocol.js";import{init_native_dispatcher as r,native_dispatcher_exports as i}from"./native-dispatcher.js";import a from"@girs/soup-3.0";import o from"@girs/gio-2.0";import s from"@girs/glib-2.0";import{EventEmitter as c}from"node:events";import{Readable as l,Writable as u}from"node:stream";import{Buffer as d}from"node:buffer";import{deferEmit as f,ensureMainLoop as p}from"@gjsify/utils";import{loadNativeHttp2 as m}from"@gjsify/http2-native";import{closeSync as h,openSync as g,read as _,statSync as v}from"node:fs";var Http2ServerRequest=class extends l{method=`GET`;url=`/`;headers={};rawHeaders=[];authority=``;scheme=`https`;httpVersion=`2.0`;httpVersionMajor=2;httpVersionMinor=0;complete=!1;socket=null;trailers={};rawTrailers=[];_stream=null;_timeoutTimer=null;get stream(){return this._stream}_setStream(e){this._stream=e}constructor(){super()}_read(e){}_autoClose(){}_pushBody(e){e&&e.length>0&&this.push(d.from(e)),this.push(null),this.complete=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null)}setTimeout(e,t){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),t&&this.once(`timeout`,t),e>0&&(this._timeoutTimer=setTimeout(()=>{this._timeoutTimer=null,this.emit(`timeout`)},e)),this}destroy(e){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),super.destroy(e)}},Http2ServerResponse=class extends u{statusCode=200;statusMessage=``;headersSent=!1;finished=!1;sendDate=!0;_soupMsg;_nativeBackend;_headers=new Map;_streaming=!1;_timeoutTimer=null;_stream=null;_detachedBody=null;get stream(){return this._stream}get socket(){return null}get isDetached(){return this._soupMsg===null&&this._nativeBackend===null}get detachedBody(){return this._detachedBody?d.concat(this._detachedBody):null}get isNative(){return this._nativeBackend!==null}get nativeBackend(){return this._nativeBackend}_setStream(e){this._stream=e}_setNativeBackend(e){this._nativeBackend=e}constructor(e,t=null){super(),this._soupMsg=e,this._nativeBackend=t,e===null&&t===null&&(this._detachedBody=[])}setHeader(e,t){return this._headers.set(e.toLowerCase(),typeof t==`number`?String(t):t),this}getHeader(e){return this._headers.get(e.toLowerCase())}removeHeader(e){this._headers.delete(e.toLowerCase())}hasHeader(e){return this._headers.has(e.toLowerCase())}getHeaderNames(){return Array.from(this._headers.keys())}getHeaders(){let e={};for(let[t,n]of this._headers)e[t]=n;return e}appendHeader(e,t){let n=e.toLowerCase(),r=this._headers.get(n);return r===void 0?this._headers.set(n,t):Array.isArray(r)?Array.isArray(t)?r.push(...t):r.push(t):this._headers.set(n,Array.isArray(t)?[r,...t]:[r,t]),this}flushHeaders(){this.headersSent||=!0}writeHead(e,t,n){if(this.statusCode=e,typeof t==`object`&&(n=t,t=void 0),typeof t==`string`&&(this.statusMessage=t),n)for(let[e,t]of Object.entries(n))this.setHeader(e,t);return this}respond(e,t){let n=Number(e[`:status`]??200),r={};for(let[t,n]of Object.entries(e))t!==`:status`&&(r[t]=typeof n==`number`?String(n):n);this.writeHead(n,r),t?.endStream&&this.end()}writeContinue(e){e&&Promise.resolve().then(e)}writeEarlyHints(e,t){t&&Promise.resolve().then(t)}addTrailers(e){}setTimeout(e,t){return this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),t&&this.once(`timeout`,t),e>0&&(this._timeoutTimer=setTimeout(()=>{this._timeoutTimer=null,this.emit(`timeout`)},e)),this}_startStreaming(){if(this._streaming)return;if(this._streaming=!0,this.headersSent=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),this._nativeBackend){this._nativeBackend.submitResponse(this.statusCode,this.statusMessage,this._headers,!1);return}if(!this._soupMsg)return;this._soupMsg.set_status(this.statusCode,this.statusMessage||null);let e=this._soupMsg.get_response_headers();this._headers.has(`content-length`)?e.set_encoding(a.Encoding.CONTENT_LENGTH):e.set_encoding(a.Encoding.CHUNKED);for(let[t,n]of this._headers)if(Array.isArray(n))for(let r of n)e.append(t,r);else e.replace(t,n)}_write(e,t,n){let r=d.isBuffer(e)?e:d.from(e,t);this._startStreaming(),this._nativeBackend?this._nativeBackend.submitData(r,!1):this._soupMsg?(this._soupMsg.get_response_body().append(new Uint8Array(r.buffer,r.byteOffset,r.byteLength)),this._soupMsg.unpause()):this._detachedBody&&this._detachedBody.push(r),n()}_final(e){this._streaming?this._nativeBackend?this._nativeBackend.submitData(d.alloc(0),!0):this._soupMsg&&(this._soupMsg.get_response_body().complete(),this._soupMsg.unpause()):this._sendBatchResponse(),this.finished=!0,e()}_sendBatchResponse(){if(this.headersSent)return;if(this.headersSent=!0,this._timeoutTimer&&=(clearTimeout(this._timeoutTimer),null),this._nativeBackend){this._nativeBackend.submitResponse(this.statusCode,this.statusMessage,this._headers,!0);return}if(!this._soupMsg)return;this._soupMsg.set_status(this.statusCode,this.statusMessage||null);let e=this._soupMsg.get_response_headers();for(let[t,n]of this._headers)if(Array.isArray(n))for(let r of n)e.append(t,r);else e.replace(t,n);let t=this._headers.get(`content-type`)||`text/plain`;this._soupMsg.set_response(t,a.MemoryUse.COPY,new Uint8Array)}end(e,t,n){return typeof e==`function`?(n=e,e=void 0):typeof t==`function`&&(n=t,t=void 0),e!=null&&this.write(e,t),super.end(n),this}respondWithFD(e,t,n){_respondFromFD(this,e,t,n??{},!1)}respondWithFile(e,t,n){let r;try{r=g(e,`r`)}catch(e){if(n?.onError){n.onError(e);return}throw e}_respondFromFD(this,r,t,n??{},!0)}pushStream(e,t,n){if(typeof t==`function`&&(n=t,t={}),!n)throw TypeError(`callback must be a function`);if(!this._stream){n(Error(`No associated stream`),null,{});return}this._stream.pushStream(e,t,n)}createPushResponse(e,t){if(typeof t!=`function`)throw TypeError(`callback must be a function`);this.pushStream(e,{},(e,n)=>{if(e){t(e,null);return}let r=n._res;t(null,r??null)})}},y=class ServerHttp2Stream extends c{id;pushAllowed;sentHeaders={};_res;_session;_isPushedStream;_pushedChildren=[];_pushPromiseFrame=null;_pushRequestHeaders=null;get session(){return this._session}get headersSent(){return this._res.headersSent}get closed(){return this._res.writableEnded}get destroyed(){return this._res.destroyed}get pending(){return!1}get state(){return this.closed?t.NGHTTP2_STREAM_STATE_CLOSED:t.NGHTTP2_STREAM_STATE_OPEN}get pushPromiseFrame(){return this._pushPromiseFrame}get pushRequestHeaders(){return this._pushRequestHeaders}get pushedChildren(){return this._pushedChildren}constructor(e,t=null,n={}){super(),this._res=e,this._session=t,this._isPushedStream=n.isPushedStream===!0,this.id=n.streamId??1,this.pushAllowed=!this._isPushedStream&&t?.canPush!==!1,e.on(`finish`,()=>this.emit(`close`)),e.on(`error`,e=>this.emit(`error`,e))}respond(e,t){this._res.respond(e,t)}write(e,t,n){return this._res.write(e,t,n)}end(e,t,n){return this._res.end(e,t,n),this}destroy(e){return this._res.destroy(e),this}close(e,n){n&&this.once(`close`,n);let r=this._res.nativeBackend;if(r)try{r.reset(e??t.NGHTTP2_NO_ERROR)}catch{}this._res.end()}priority(e){}setTimeout(e,t){return this._res.setTimeout(e,t),this}sendTrailers(e){}additionalHeaders(e){}respondWithFD(e,t,n){this._res.respondWithFD(e,t,n)}respondWithFile(e,t,n){this._res.respondWithFile(e,t,n)}pushStream(e,t,n){if(typeof t==`function`&&(n=t,t={}),!n)throw TypeError(`callback must be a function`);if(this._isPushedStream){let e=Object.assign(Error(`Cannot initiate nested push streams`),{code:`ERR_HTTP2_NESTED_PUSH`});n(e,null,{});return}if(this._session&&this._session.canPush===!1){let e=Object.assign(Error(`HTTP/2 server push has been disabled`),{code:`ERR_HTTP2_PUSH_DISABLED`});n(e,null,{});return}let r,i=null,a={},o={};for(let[t,n]of Object.entries(e))o[t]=typeof n==`number`?String(n):n;o[`:method`]||=`GET`,a=o;let s=this._res.nativeBackend,c=null;if(s){if(c=s.pushPromise(o),!c){let e=Object.assign(Error(`No available stream ids`),{code:`ERR_HTTP2_OUT_OF_STREAMS`});n(e,null,{});return}r=c.streamId,i=null}else if(this._session){if(r=this._session._allocatePushId(),r===0){let e=Object.assign(Error(`No available stream ids`),{code:`ERR_HTTP2_OUT_OF_STREAMS`});n(e,null,{});return}i=this._session._buildPushPromise(this.id,r,o)}else r=2;let l=c?new Http2ServerResponse(null,c):new Http2ServerResponse(_makeDetachedSoupMessage()),u=new ServerHttp2Stream(l,this._session,{isPushedStream:!0,streamId:r});u._pushPromiseFrame=i,u._pushRequestHeaders=o,l._setStream(u),this._pushedChildren.push(u),Promise.resolve().then(()=>{n(null,u,a)})}},ServerHttp2Session=class extends c{type=t.NGHTTP2_SESSION_SERVER;alpnProtocol=`h2`;encrypted=!0;_closed=!1;_destroyed=!1;_settings;_canPush=!0;_frameEncoder=null;_streamIdAllocator=null;_fallbackPushId=2;constructor(){super(),this._settings=n()}get canPush(){return this._canPush}set canPush(e){this._canPush=e}_allocatePushId(){let e=m();if(e)return this._streamIdAllocator||=e.StreamIdAllocator.new(),this._streamIdAllocator.next_promised();let t=this._fallbackPushId;return t>2147483647?0:(this._fallbackPushId+=2,t)}_buildPushPromise(e,t,n){let r=m();if(!r)return null;this._frameEncoder||=r.FrameEncoder.new();let i=[],a=[];for(let[e,t]of Object.entries(n)){let n=e.toLowerCase();if(Array.isArray(t))for(let e of t)i.push(n),a.push(String(e));else i.push(n),a.push(String(t))}let o=this._frameEncoder.encode_headers(i,a);if(!o)return null;let s=this._frameEncoder.build_push_promise(e,t,o),c=s.toArray;if(typeof c==`function`)return c.call(s);let l=s.get_data;return typeof l==`function`?l.call(s)??null:null}get closed(){return this._closed}get destroyed(){return this._destroyed}get pendingSettingsAck(){return!1}get localSettings(){return{...this._settings}}get remoteSettings(){return n()}get originSet(){return[]}settings(e,t){Object.assign(this._settings,e),t&&Promise.resolve().then(t)}goaway(e,n,r){this.emit(`goaway`,e??t.NGHTTP2_NO_ERROR),this.destroy()}ping(e,t){let n=new Uint8Array(8);return t&&Promise.resolve().then(()=>t(null,0,n)),!0}close(e){this._closed||(this._closed=!0,this.emit(`close`),e&&e())}destroy(e,t){this._destroyed||(this._destroyed=!0,this._closed=!0,e&&this.emit(`error`,e),t!==void 0&&this.emit(`goaway`,t),this.emit(`close`))}altsvc(e,t){}origin(...e){}ref(){}unref(){}};const b=new Set;var Http2Server=class extends c{listening=!1;maxHeadersCount=2e3;timeout=0;_soupServer=null;_nativeDispatcher=null;_address=null;_options;get soupServer(){return this._soupServer}get nativeDispatcher(){return this._nativeDispatcher}constructor(e,t){super(),typeof e==`function`&&(t=e,e={}),this._options=e??{},t&&this.on(`request`,t)}listen(...e){let t=0,n=`0.0.0.0`,r;for(let i of e)typeof i==`number`?t=i:typeof i==`string`?n=i:typeof i==`function`&&(r=i);r&&this.once(`listening`,r);try{let e=this._options.nativeDispatcher??`auto`,r=e===`force`||e===`auto`&&this._options.allowHTTP1===!1;if(e===`off`&&this._options.allowHTTP1===!1)throw Error(`createServer({ allowHTTP1: false }) requires the native dispatcher; nativeDispatcher cannot be "off" in this configuration`);if(r)return this._startNativeListen(t,n),p(),f(this,`listening`),b.add(this),this;this._soupServer=new a.Server({}),this._configureSoupServer(this._soupServer),this._soupServer.add_handler(null,(e,t,n)=>{this._handleRequest(t)}),this._soupServer.listen_local(t,a.ServerListenOptions.IPV4_ONLY),p();let i=this._soupServer.get_listeners(),o=t;if(i&&i.length>0){let e=i[0].get_local_address();e&&typeof e.get_port==`function`&&(o=e.get_port())}this.listening=!0,this._address={port:o,family:`IPv4`,address:n},b.add(this),f(this,`listening`)}catch(e){let t=e instanceof Error?e:Error(String(e));if(this.listenerCount(`error`)===0)throw t;f(this,`error`,t)}return this}_startNativeListen(t,n){let{Http2NativeDispatcher:a}=(r(),e(i));if(!a.available())throw Error(`@gjsify/http2-native prebuild is not loadable. createServer({ allowHTTP1: false }) requires the native HTTP/2 dispatcher. Ensure GjsifyHttp2-1.0.typelib is installed.`);this._nativeDispatcher=new a({handler:e=>this._handleNativeStream(e)});let o=this._nativeDispatcher.listen(t);this.listening=!0,this._address={port:o,family:`IPv4`,address:n}}_handleNativeStream(e){let t=new Http2ServerRequest,adapt=e=>({streamId:e.streamId,submitResponse:(t,n,r,i)=>{let a={":status":t};for(let[e,t]of r)a[e]=t;e.respond(a,i)},submitData:(t,n)=>e.writeData(t,n),reset:t=>e.reset(t),pushPromise:t=>{let n=e.pushPromise(t);return n?adapt(n):null}}),n=new Http2ServerResponse(null,adapt(e.backend)),r=new y(n,new ServerHttp2Session,{streamId:e.streamId});t._setStream(r),n._setStream(r);let i=e.headers;t.method=String(i[`:method`]??`GET`),t.url=String(i[`:path`]??`/`),t.authority=String(i[`:authority`]??``),t.scheme=String(i[`:scheme`]??`http`),t.httpVersion=`2.0`,t.httpVersionMajor=2,t.httpVersionMinor=0;for(let[e,n]of Object.entries(i))if(!e.startsWith(`:`))if(t.headers[e]=n,Array.isArray(n))for(let r of n)t.rawHeaders.push(e,r);else t.rawHeaders.push(e,n);t.socket={remoteAddress:e.remoteAddress,remotePort:e.remotePort,localAddress:this._address?.address??`127.0.0.1`,localPort:e.localPort,encrypted:!1},(async()=>{try{for await(let n of e.body)t._pushBody(n);t._pushBody(null)}catch{t._pushBody(null)}})();let a={...i};this.emit(`stream`,r,a),this.emit(`request`,t,n)}_configureSoupServer(e){}_handleRequest(e){let t=new Http2ServerRequest,n=new Http2ServerResponse(e);t.method=e.get_method();let r=e.get_uri(),i=r.get_path(),s=r.get_query();t.url=s?i+`?`+s:i,t.authority=r.get_host()??``,t.scheme=r.get_scheme()??`http`,e.get_http_version()===a.HTTPVersion.HTTP_2_0?(t.httpVersion=`2.0`,t.httpVersionMajor=2,t.httpVersionMinor=0):(t.httpVersion=`1.1`,t.httpVersionMajor=1,t.httpVersionMinor=1),e.get_request_headers().foreach((e,n)=>{let r=e.toLowerCase();if(t.rawHeaders.push(e,n),r in t.headers){let e=t.headers[r];Array.isArray(e)?e.push(n):t.headers[r]=[e,n]}else t.headers[r]=n});let c=e.get_remote_host()??`127.0.0.1`,l=e.get_remote_address();t.socket={remoteAddress:c,remotePort:l instanceof o.InetSocketAddress?l.get_port():0,localAddress:this._address?.address??`127.0.0.1`,localPort:this._address?.port??0,encrypted:this instanceof Http2SecureServer};let u=e.get_request_body();u?.data&&u.data.length>0?t._pushBody(u.data):t._pushBody(null);let d={":method":t.method,":path":t.url,":authority":t.authority,":scheme":t.scheme,...t.headers};e.pause(),n.on(`finish`,()=>e.unpause());let f=new y(n,new ServerHttp2Session);t._setStream(f),n._setStream(f),this.emit(`stream`,f,d),this.emit(`request`,t,n)}address(){return this._address}close(e){return e&&this.once(`close`,e),this._soupServer&&=(this._soupServer.disconnect(),null),this._nativeDispatcher&&=(this._nativeDispatcher.close(),null),this.listening=!1,b.delete(this),f(this,`close`),this}setTimeout(e,t){return this.timeout=e,t&&this.on(`timeout`,t),this}},Http2SecureServer=class extends Http2Server{_tlsCert=null;constructor(e,t){if(super(e,t),e.cert&&e.key){let t=_toPemString(e.cert),n=_toPemString(e.key);this._tlsCert=_createTlsCertificate(t,n)}else e.pfx}_configureSoupServer(e){this._tlsCert&&e.set_tls_certificate(this._tlsCert)}setSecureContext(e){if(e.cert&&e.key){let t=_toPemString(e.cert),n=_toPemString(e.key);this._tlsCert=_createTlsCertificate(t,n),this._soupServer&&this._tlsCert&&this._soupServer.set_tls_certificate(this._tlsCert)}}};function _toPemString(e){return Array.isArray(e)?e.map(_toPemString).join(`
2
+ `):d.isBuffer(e)?e.toString(`utf8`):e}function _createTlsCertificate(e,t){let n=e.trimEnd()+`
3
3
  `+t.trimEnd()+`
4
- `;try{return r.TlsCertificate.new_from_pem(n,-1)}catch{let n=i.get_tmp_dir(),a=i.build_filenamev([n,`gjsify-http2-cert.pem`]),o=i.build_filenamev([n,`gjsify-http2-key.pem`]);try{return i.file_set_contents(a,e),i.file_set_contents(o,t),r.TlsCertificate.new_from_files(a,o)}finally{try{r.File.new_for_path(a).delete(null)}catch{}try{r.File.new_for_path(o).delete(null)}catch{}}}}function _makeDetachedSoupMessage(){return null}function _respondFromFD(e,t,n,r,i){let a=typeof t==`number`?t:t.fd,o=a,s={...n??{}};if(r.statCheck)try{let t=m(_fdPath(a)??`/proc/self/fd/`+a);if(r.statCheck(t,s,r)===!1){i&&d(a),e.end();return}}catch(e){if(r.onError){r.onError(e),i&&d(a);return}}let l=Number(s[`:status`]??200);delete s[`:status`];let u={};for(let[e,t]of Object.entries(s))u[e]=typeof t==`number`?String(t):t;e.writeHead(l,u),e.flushHeaders();let f=Math.max(0,r.offset??0),h=r.length,g=64*1024,_=c.alloc(g),v=f,y=typeof h==`number`?h:1/0,b=0,readNext=()=>{if(y<=0){finish();return}p(o,_,0,Math.min(g,y),v,(t,n)=>{if(t){cleanup(t);return}if(n===0){finish();return}v+=n,b+=n,y-=n;let r=c.allocUnsafe(n);_.copy(r,0,0,n),e.write(r)?readNext():e.once(`drain`,readNext)})},finish=()=>{if(e.end(),i)try{d(o)}catch{}},cleanup=t=>{if(r.onError?r.onError(t):e.destroy(t),i)try{d(o)}catch{}};if(y===0){finish();return}readNext()}function _fdPath(e){return typeof e!=`number`||e<0?null:`/proc/self/fd/`+e}export{Http2SecureServer,Http2Server,Http2ServerRequest,Http2ServerResponse,ServerHttp2Session,g as ServerHttp2Stream};
4
+ `;try{return o.TlsCertificate.new_from_pem(n,-1)}catch{let n=s.get_tmp_dir(),r=s.build_filenamev([n,`gjsify-http2-cert.pem`]),i=s.build_filenamev([n,`gjsify-http2-key.pem`]);try{return s.file_set_contents(r,e),s.file_set_contents(i,t),o.TlsCertificate.new_from_files(r,i)}finally{try{o.File.new_for_path(r).delete(null)}catch{}try{o.File.new_for_path(i).delete(null)}catch{}}}}function _makeDetachedSoupMessage(){return null}function _respondFromFD(e,t,n,r,i){let a=typeof t==`number`?t:t.fd,o=a,s={...n??{}};if(r.statCheck)try{let t=v(_fdPath(a)??`/proc/self/fd/`+a);if(r.statCheck(t,s,r)===!1){i&&h(a),e.end();return}}catch(e){if(r.onError){r.onError(e),i&&h(a);return}}let c=Number(s[`:status`]??200);delete s[`:status`];let l={};for(let[e,t]of Object.entries(s))l[e]=typeof t==`number`?String(t):t;e.writeHead(c,l),e.flushHeaders();let u=Math.max(0,r.offset??0),f=r.length,p=64*1024,m=d.alloc(p),g=u,y=typeof f==`number`?f:1/0,b=0,readNext=()=>{if(y<=0){finish();return}_(o,m,0,Math.min(p,y),g,(t,n)=>{if(t){cleanup(t);return}if(n===0){finish();return}g+=n,b+=n,y-=n;let r=d.allocUnsafe(n);m.copy(r,0,0,n),e.write(r)?readNext():e.once(`drain`,readNext)})},finish=()=>{if(e.end(),i)try{h(o)}catch{}},cleanup=t=>{if(r.onError?r.onError(t):e.destroy(t),i)try{h(o)}catch{}};if(y===0){finish();return}readNext()}function _fdPath(e){return typeof e!=`number`||e<0?null:`/proc/self/fd/`+e}export{Http2SecureServer,Http2Server,Http2ServerRequest,Http2ServerResponse,ServerHttp2Session,y as ServerHttp2Stream};
@@ -20,6 +20,20 @@ export interface ClientSessionOptions {
20
20
  cert?: string | Buffer | Array<string | Buffer>;
21
21
  key?: string | Buffer | Array<string | Buffer>;
22
22
  ALPNProtocols?: string[];
23
+ /**
24
+ * Native dispatcher mode (gjsify-specific, defaults to `'auto'`).
25
+ *
26
+ * - `'auto'` — use the @gjsify/http2-native client for `http://` (h2c)
27
+ * URLs when the prebuild is loadable; route `https://` through Soup
28
+ * (h2 negotiated transparently via ALPN — works fine, no need for
29
+ * the native path).
30
+ * - `'force'` — always use the native client; throws if unavailable.
31
+ * Surfaces server-pushed streams ('stream' event on the session) that
32
+ * Soup's high-level Session API doesn't expose.
33
+ * - `'off'` — never use the native client; keep Soup even for h2c.
34
+ * Result: h2c requests fail at the protocol level.
35
+ */
36
+ nativeDispatcher?: 'auto' | 'force' | 'off';
23
37
  }
24
38
  export interface ClientStreamOptions {
25
39
  endStream?: boolean;
@@ -53,7 +67,7 @@ export declare class Http2Session extends EventEmitter {
53
67
  unref(): void;
54
68
  }
55
69
  export declare class ClientHttp2Stream extends Duplex {
56
- readonly id = 1;
70
+ private _id;
57
71
  readonly pending = false;
58
72
  readonly aborted = false;
59
73
  readonly bufferSize = 0;
@@ -64,14 +78,25 @@ export declare class ClientHttp2Stream extends Duplex {
64
78
  private _cancellable;
65
79
  private _state;
66
80
  private _responseHeaders;
81
+ /** Native dispatcher stream id when the request rides the native path. */
82
+ private _nativeStreamId;
83
+ /** Returns the client-allocated stream id. Native path: real nghttp2 id;
84
+ * Soup path: hard-coded 1 (Soup multiplexes opaquely). */
85
+ get id(): number;
67
86
  get state(): number;
68
87
  get rstCode(): number;
69
88
  get session(): ClientHttp2Session;
70
89
  get sentHeaders(): Record<string, string | string[]>;
90
+ /** @internal Hook used by the native client dispatcher to bind a stream to
91
+ * a server-pushed promised id (pushed streams arrive with their id
92
+ * already allocated by the peer; we don't submit_request for them). */
93
+ _setNativeStreamId(streamId: number): void;
71
94
  constructor(session: ClientHttp2Session, requestHeaders: Record<string, string | string[]>);
72
95
  _read(_size: number): void;
73
96
  _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
74
97
  _final(callback: (error?: Error | null) => void): void;
98
+ /** Wire response headers + body iteration when running on the native path. */
99
+ private _wireNativeBody;
75
100
  private _sendRequest;
76
101
  close(code?: number, callback?: () => void): void;
77
102
  priority(_options: {
@@ -89,9 +114,14 @@ export declare class ClientHttp2Session extends Http2Session {
89
114
  private _authority;
90
115
  private _soupSession;
91
116
  private _streams;
117
+ private _nativeClient;
118
+ get nativeClient(): import('./native-client-dispatcher.js').Http2NativeClientDispatcher | null;
92
119
  constructor(authority: string, options?: ClientSessionOptions);
120
+ private _setupNativeClient;
93
121
  /** @internal Used by ClientHttp2Stream to get the Soup.Session */
94
122
  _getSoupSession(): Soup.Session;
123
+ /** @internal Used by ClientHttp2Stream to access the native client dispatcher. */
124
+ _getNativeClient(): import('./native-client-dispatcher.js').Http2NativeClientDispatcher | null;
95
125
  /** @internal Used by ClientHttp2Stream to build the request URL */
96
126
  _getAuthority(): string;
97
127
  request(headers: Record<string, string | string[]>, options?: ClientStreamOptions): ClientHttp2Stream;
@@ -0,0 +1 @@
1
+ export default function (): Promise<void>;
@@ -0,0 +1,56 @@
1
+ export interface NativeClientStreamEvent {
2
+ streamId: number;
3
+ headers: Record<string, string | string[]>;
4
+ endStream: boolean;
5
+ body: AsyncIterable<Buffer>;
6
+ }
7
+ export interface NativeClientPushPromiseEvent {
8
+ parentStreamId: number;
9
+ promisedStreamId: number;
10
+ headers: Record<string, string | string[]>;
11
+ }
12
+ export interface NativeClientCallbacks {
13
+ onResponse?: (event: NativeClientStreamEvent) => void;
14
+ onPushPromise?: (event: NativeClientPushPromiseEvent) => void;
15
+ onGoaway?: (lastStreamId: number, errorCode: number) => void;
16
+ onClose?: () => void;
17
+ }
18
+ export declare class Http2NativeClientDispatcher {
19
+ private _bridge;
20
+ private _connection;
21
+ private _inSource;
22
+ private _streams;
23
+ private _closed;
24
+ private _callbacks;
25
+ private _connectError;
26
+ constructor(callbacks?: NativeClientCallbacks);
27
+ static available(): boolean;
28
+ /**
29
+ * Synchronously connect to `host:port`. Throws on connection failure or
30
+ * if the native bridge isn't loadable. The session preface + initial
31
+ * SETTINGS are sent immediately.
32
+ */
33
+ connect(host: string, port: number): void;
34
+ /**
35
+ * Submit a new request stream. Returns the freshly-allocated odd client
36
+ * stream id and an async iterator for response DATA. Headers arrive via
37
+ * the `onResponse` callback once the server emits HEADERS.
38
+ *
39
+ * The caller is expected to follow up with `writeData()` (one or more
40
+ * times, finally with `endStream: true`) if the request has a body.
41
+ */
42
+ submitRequest(headers: Record<string, string | string[]>, endStream: boolean): number;
43
+ writeData(streamId: number, chunk: Buffer, endStream: boolean): void;
44
+ /** Iterate response body chunks for a given stream id. */
45
+ body(streamId: number): AsyncIterable<Buffer>;
46
+ /** Read pseudo-headers + regular headers received for a stream. */
47
+ responseHeaders(streamId: number): Record<string, string | string[]> | null;
48
+ close(): void;
49
+ private _close;
50
+ private _flushOutput;
51
+ private _onHeaders;
52
+ private _onData;
53
+ private _onStreamClosed;
54
+ private _onPushPromise;
55
+ private _registerStreamForPush;
56
+ }
@@ -0,0 +1,92 @@
1
+ import Gio from 'gi://Gio?version=2.0';
2
+ /**
3
+ * The dispatcher emits incoming requests by invoking this handler. The shape
4
+ * matches what `Http2Server._handleNativeStream()` consumes — explicit
5
+ * separation so `server.ts` can keep its existing Soup hooks intact and the
6
+ * native path just plugs into the same emitter surface.
7
+ */
8
+ export interface NativeStreamHandler {
9
+ (event: NativeStreamEvent): void;
10
+ }
11
+ /**
12
+ * Per-stream submit handle the dispatcher hands to its caller. Used by
13
+ * `@gjsify/http2`'s server.ts to route response writes, DATA frames, and
14
+ * push-promise allocations through the SessionBridge that owns the
15
+ * underlying TCP connection.
16
+ *
17
+ * Pushed streams reuse the parent's connection (single TCP socket per
18
+ * HTTP/2 session) — `pushPromise()` allocates a server-side stream-id
19
+ * via `SessionBridge.submit_push_promise()` and returns a sibling backend
20
+ * the caller can use to dispatch the pushed response.
21
+ */
22
+ export interface NativeStreamBackend {
23
+ /** Connection-unique stream id this backend writes to. */
24
+ readonly streamId: number;
25
+ /** Submit a response (status + headers) back through the wire. */
26
+ respond(headers: Record<string, string | number | string[]>, endStream: boolean): void;
27
+ /** Submit a DATA frame for this stream. */
28
+ writeData(chunk: Buffer, endStream: boolean): void;
29
+ /** RST_STREAM to abort. */
30
+ reset(errorCode?: number): void;
31
+ /**
32
+ * Submit a PUSH_PROMISE on this stream. Returns the freshly-allocated
33
+ * pushed-stream backend (null on error / push disabled). The caller
34
+ * then dispatches headers + body through the returned backend.
35
+ */
36
+ pushPromise(headers: Record<string, string | number | string[]>): NativeStreamBackend | null;
37
+ }
38
+ export interface NativeStreamEvent {
39
+ /** Connection-unique stream id (odd, client-initiated). */
40
+ streamId: number;
41
+ /** Pseudo-headers + regular request headers, lower-cased names. */
42
+ headers: Record<string, string | string[]>;
43
+ /** %TRUE if the HEADERS frame also carried END_STREAM (no body to follow). */
44
+ endStream: boolean;
45
+ /** Remote peer address — populated for `req.socket` in the server adapter. */
46
+ remoteAddress: string;
47
+ /** Remote peer port. */
48
+ remotePort: number;
49
+ /** Local listening port. */
50
+ localPort: number;
51
+ /** Async iterator yielding DATA chunks for this stream. */
52
+ body: AsyncIterable<Buffer>;
53
+ /** Per-stream submit handle. Use this for response/data/push-promise. */
54
+ backend: NativeStreamBackend;
55
+ }
56
+ export declare class Http2NativeDispatcher {
57
+ private _service;
58
+ private _connections;
59
+ private _listenPort;
60
+ private _handler;
61
+ /** When true, the dispatcher peeks 24 bytes per accepted socket and routes
62
+ * non-preface connections back to the fallback callback (Soup HTTP/1.1). */
63
+ private _prefaceGate;
64
+ /** Called when the peeked bytes don't match the h2c preface — caller hands
65
+ * the socket to Soup (or whatever the fallback path is). */
66
+ private _onNonH2c;
67
+ constructor(opts: {
68
+ handler: NativeStreamHandler;
69
+ prefaceGate?: boolean;
70
+ onNonH2c?: (connection: Gio.SocketConnection, peeked: Uint8Array) => void;
71
+ });
72
+ /** Whether the GjsifyHttp2 typelib is loadable in this process. */
73
+ static available(): boolean;
74
+ listen(port: number): number;
75
+ close(): void;
76
+ private _acceptConnection;
77
+ private _setupNativeConnection;
78
+ private _flushOutput;
79
+ private _onHeaders;
80
+ private _onData;
81
+ private _onStreamClosed;
82
+ /**
83
+ * Build a per-stream submit handle on the given connection. Pushed
84
+ * streams (allocated via `submit_push_promise`) share the underlying
85
+ * `SessionBridge` so they can reuse this same factory.
86
+ */
87
+ private _makeBackend;
88
+ private _submitResponse;
89
+ private _submitData;
90
+ private _makeBodyIterator;
91
+ private _closeConnection;
92
+ }
@@ -29,6 +29,24 @@ export declare class Http2ServerRequest extends Readable {
29
29
  setTimeout(msecs: number, callback?: () => void): this;
30
30
  destroy(error?: Error): this;
31
31
  }
32
+ /**
33
+ * Per-stream backend that routes writes through `SessionBridge.submit_*`
34
+ * instead of into a Soup message. Set on responses produced by the native
35
+ * dispatcher (Phase 1+). When `_nativeBackend` is non-null,
36
+ * `Http2ServerResponse` ignores its `_soupMsg` field (also null in that
37
+ * case) and dispatches every operation through this object.
38
+ *
39
+ * Pushed streams reuse the same connection — `pushPromise()` returns a
40
+ * sibling backend for the freshly-allocated promised stream id.
41
+ */
42
+ export interface Http2NativeBackend {
43
+ streamId: number;
44
+ submitResponse(statusCode: number, statusMessage: string, headers: Map<string, string | string[]>, endStream: boolean): void;
45
+ submitData(chunk: Buffer, endStream: boolean): void;
46
+ reset(errorCode: number): void;
47
+ /** Allocate a pushed stream-id; returns a child backend or null on error. */
48
+ pushPromise(headers: Record<string, string | number | string[]>): Http2NativeBackend | null;
49
+ }
32
50
  export declare class Http2ServerResponse extends Writable {
33
51
  statusCode: number;
34
52
  statusMessage: string;
@@ -36,6 +54,7 @@ export declare class Http2ServerResponse extends Writable {
36
54
  finished: boolean;
37
55
  sendDate: boolean;
38
56
  private _soupMsg;
57
+ private _nativeBackend;
39
58
  private _headers;
40
59
  private _streaming;
41
60
  private _timeoutTimer;
@@ -48,8 +67,14 @@ export declare class Http2ServerResponse extends Writable {
48
67
  get isDetached(): boolean;
49
68
  /** Buffered body bytes for detached (push) responses — null on regular responses. */
50
69
  get detachedBody(): Buffer | null;
70
+ /** Whether this response routes through the native HTTP/2 dispatcher. */
71
+ get isNative(): boolean;
72
+ /** @internal — used by `ServerHttp2Stream.pushStream()` to allocate a pushed child backend. */
73
+ get nativeBackend(): Http2NativeBackend | null;
51
74
  _setStream(stream: ServerHttp2Stream): void;
52
- constructor(soupMsg: Soup.ServerMessage | null);
75
+ /** @internal Used by the native dispatcher to attach its submit backend. */
76
+ _setNativeBackend(backend: Http2NativeBackend): void;
77
+ constructor(soupMsg: Soup.ServerMessage | null, nativeBackend?: Http2NativeBackend | null);
53
78
  setHeader(name: string, value: string | number | string[]): this;
54
79
  getHeader(name: string): string | string[] | undefined;
55
80
  removeHeader(name: string): void;
@@ -251,23 +276,45 @@ export interface ServerOptions {
251
276
  Http1IncomingMessage?: any;
252
277
  Http1ServerResponse?: any;
253
278
  unknownProtocolTimeout?: number;
279
+ /**
280
+ * Native dispatcher mode (gjsify-specific, defaults to `'auto'`).
281
+ *
282
+ * - `'auto'` — use the @gjsify/http2-native dispatcher when available and
283
+ * the call is for cleartext HTTP/2 (`createServer({allowHTTP1: false})`)
284
+ * or h2 ALPN over TLS. Falls back to Soup HTTP/1.1 otherwise.
285
+ * - `'force'` — always use the native dispatcher; throws if the prebuild
286
+ * is missing. Useful for tests + integration with raw nghttp2 clients.
287
+ * - `'off'` — never use the dispatcher; keep the Soup path even for h2c.
288
+ * `createServer({allowHTTP1: false})` then has no working configuration
289
+ * and listen() will throw.
290
+ */
291
+ nativeDispatcher?: 'auto' | 'force' | 'off';
254
292
  }
255
293
  export declare class Http2Server extends EventEmitter {
256
294
  listening: boolean;
257
295
  maxHeadersCount: number;
258
296
  timeout: number;
259
297
  protected _soupServer: Soup.Server | null;
298
+ protected _nativeDispatcher: import('./native-dispatcher.js').Http2NativeDispatcher | null;
260
299
  protected _address: {
261
300
  port: number;
262
301
  family: string;
263
302
  address: string;
264
303
  } | null;
265
- private _options;
304
+ protected _options: ServerOptions;
266
305
  get soupServer(): Soup.Server | null;
306
+ get nativeDispatcher(): import('./native-dispatcher.js').Http2NativeDispatcher | null;
267
307
  constructor(options?: ServerOptions | ((req: Http2ServerRequest, res: Http2ServerResponse) => void), handler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void);
268
308
  listen(port?: number, hostname?: string, backlog?: number, callback?: () => void): this;
269
309
  listen(port?: number, hostname?: string, callback?: () => void): this;
270
310
  listen(port?: number, callback?: () => void): this;
311
+ /**
312
+ * Native dispatcher takes over the listen socket. Soup is not involved.
313
+ * Used by createServer({allowHTTP1: false}) (h2c).
314
+ */
315
+ private _startNativeListen;
316
+ /** @internal Handler for streams arriving on the native dispatcher. */
317
+ private _handleNativeStream;
271
318
  protected _configureSoupServer(_server: Soup.Server): void;
272
319
  private _handleRequest;
273
320
  address(): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/http2",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Node.js http2 module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -33,8 +33,8 @@
33
33
  "http2"
34
34
  ],
35
35
  "devDependencies": {
36
- "@gjsify/cli": "workspace:^",
37
- "@gjsify/unit": "workspace:^",
36
+ "@gjsify/cli": "^0.4.4",
37
+ "@gjsify/unit": "^0.4.4",
38
38
  "@types/node": "^25.6.2",
39
39
  "typescript": "^6.0.3"
40
40
  },
@@ -43,8 +43,8 @@
43
43
  "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
44
44
  "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
45
45
  "@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
46
- "@gjsify/events": "workspace:^",
47
- "@gjsify/http2-native": "workspace:^",
48
- "@gjsify/utils": "workspace:^"
46
+ "@gjsify/events": "^0.4.4",
47
+ "@gjsify/http2-native": "^0.4.4",
48
+ "@gjsify/utils": "^0.4.4"
49
49
  }
50
50
  }