@gjsify/ws 0.52.0 → 0.53.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/README.md CHANGED
@@ -33,6 +33,19 @@ wss.on('connection', (client) => {
33
33
 
34
34
  `WebSocketServer` supports `{ port }`, `{ server }` shared-port, and `{ noServer: true }` + `handleUpgrade()` modes, as well as `verifyClient`, `handleProtocols`, and `createWebSocketStream` (Duplex bridge). Validated against the Autobahn test suite: 510 OK / 4 NON-STRICT / 3 INFO / 0 FAILED.
35
35
 
36
+ ## Differences from npm `ws`
37
+
38
+ - **Close codes libsoup refuses.** libsoup puts only RFC 6455's original codes in a Close
39
+ frame, and not 1011 from a client or 1010 from a server. For those, and for 1012–1014 from
40
+ either side, `close(code)` sends **1002** (protocol error) instead of the requested code — the
41
+ code libsoup itself falls back to. The connection still closes, and both sides' `'close'`
42
+ report 1002. A peer's 1012–1014 arrives as its code, but libsoup
43
+ answers it with 1002. A libsoup limit, tracked in the repo's `status/upstream-patch-candidates.md`.
44
+ - **TLS client options.** `rejectUnauthorized` is honoured (as is
45
+ `NODE_TLS_REJECT_UNAUTHORIZED=0`); `agent`, `ca`, `cert`, `key`, `passphrase`, `pfx`, `crl`,
46
+ `ciphers`, `secureProtocol`, `maxPayload`, `followRedirects`, `maxRedirects`,
47
+ `skipUTF8Validation` and `allowSynchronousEvents` are not.
48
+
36
49
  ## License
37
50
 
38
51
  MIT
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{Buffer as e}from"@gjsify/buffer";function isValidStatusCode(e){return e>=1e3&&e<=1014&&e!==1004&&e!==1005&&e!==1006||e>=3e3&&e<=4999}function closeReason(t,n){if(t!==void 0&&(typeof t!=`number`||!isValidStatusCode(t)))throw TypeError(`First argument must be a valid error code number`);if(n===void 0)return;let r=e.isBuffer(n)?n.toString(`utf8`):String(n);if(e.byteLength(r)>123)throw RangeError(`The message must not be greater than 123 bytes`);return r}export{closeReason,isValidStatusCode};
@@ -1,4 +1,4 @@
1
- import"./_virtual/_rolldown/runtime.js";import"./constants.js";import{EventEmitter as e}from"@gjsify/events";import{Buffer as t}from"@gjsify/buffer";import{createHash as n}from"@gjsify/crypto";import r from"@girs/soup-3.0";import i from"@girs/glib-2.0";import a from"@girs/gio-2.0";import{ensureMainLoop as o}from"@gjsify/utils/core";const s=/^[+/0-9A-Za-z]{22}==$/;var ServerSideWebSocket=class extends e{static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3;CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState=1;protocol=``;extensions=``;url=``;_conn;constructor(e,n){super(),this._conn=e,this.url=n,e.connect(`message`,(e,n,i)=>{let a=i.get_data();if(n===r.WebsocketDataType.TEXT){let e=typeof a==`string`?a:a?new TextDecoder(`utf-8`).decode(a):``;this.emit(`message`,e,!1)}else{let e=a?t.from(a):t.alloc(0);this.emit(`message`,e,!0)}}),e.connect(`closed`,()=>{this.readyState=3;let n=e.get_close_code()||1005,r=e.get_close_data()||``;this.emit(`close`,n,t.from(r))}),e.connect(`error`,(e,t)=>{this.emit(`error`,Error(t.message))})}send(e,n,a){let o=typeof n==`function`?n:a;try{if(typeof e==`string`){let t=new TextEncoder().encode(e);this._conn.send_message(r.WebsocketDataType.TEXT,new i.Bytes(t))}else{let n;if(t.isBuffer(e)){let t=e;n=new i.Bytes(new Uint8Array(t.buffer,t.byteOffset,t.byteLength))}else if(e instanceof ArrayBuffer)n=new i.Bytes(new Uint8Array(e));else if(ArrayBuffer.isView(e)){let t=e;n=new i.Bytes(new Uint8Array(t.buffer,t.byteOffset,t.byteLength))}else throw TypeError(`Unsupported send() payload type`);this._conn.send_message(r.WebsocketDataType.BINARY,n)}o&&queueMicrotask(()=>o())}catch(e){let t=e instanceof Error?e:Error(String(e));queueMicrotask(o?()=>o(t):()=>this.emit(`error`,t))}}close(e,n){if(!(this.readyState===3||this.readyState===2)){this.readyState=2;try{let r=n===void 0?null:t.isBuffer(n)?n.toString(`utf8`):String(n);this._conn.close(e??1e3,r)}catch(e){this.emit(`error`,e instanceof Error?e:Error(String(e)))}}}terminate(){this.readyState!==3&&(this.readyState=2,this._conn.close(1006,null))}},WebSocketServer=class extends e{options;clients=new Set;path;_server=null;_address=null;constructor(e={},t){if(super(),this.options=e,this.path=e.path??`/`,e.noServer){if(e.port!==void 0||e.server!==void 0)throw Error(`ws.WebSocketServer: { noServer: true } is mutually exclusive with port and server.`);t&&this.once(`listening`,t);return}if(e.port===void 0&&!e.server)throw Error(`ws.WebSocketServer requires either options.port or options.server on Gjs.`);t&&this.once(`listening`,t),this._start(e)}_buildVerifyClientInfo(e){let t=e.get_request_headers(),n={};t.foreach((e,t)=>{let r=e.toLowerCase(),i=n[r];i===void 0?n[r]=t:Array.isArray(i)?i.push(t):n[r]=[i,t]});let r=e.get_uri(),i=r.get_path()??`/`,o=r.get_query(),s=o?`${i}?${o}`:i,c=e.get_remote_host()??`127.0.0.1`,l=e.get_remote_address(),u=l instanceof a.InetSocketAddress?l.get_port():0;return{origin:n.origin??``,secure:!1,req:{method:e.get_method(),url:s,headers:n,socket:{remoteAddress:c,remotePort:u}}}}_setupHandlers(e,t){try{e.remove_websocket_extension(r.WebsocketExtensionDeflate.$gtype)}catch{}if(t.verifyClient){let n=t.verifyClient;e.add_handler(this.path,(e,t)=>{if((t.get_request_headers().get_one(`Upgrade`)??``).toLowerCase()!==`websocket`)return;let r=this._buildVerifyClientInfo(t);n.length>=2?(t.pause(),n(r,(e,n=401)=>{e||t.set_status(n,null),t.unpause()})):n(r)||t.set_status(401,null)})}e.add_websocket_handler(this.path,null,null,(e,n,r,i)=>{let a=new ServerSideWebSocket(i,n.get_uri()?.to_string()??this.path);if(t.handleProtocols){let e=n.get_request_headers().get_one(`Sec-WebSocket-Protocol`)??``,r=new Set(e.split(`,`).map(e=>e.trim()).filter(Boolean)),i=this._buildVerifyClientInfo(n).req,o=t.handleProtocols(r,i);o&&(a.protocol=o)}t.clientTracking!==!1&&(this.clients.add(a),a.on(`close`,()=>this.clients.delete(a))),this.emit(`connection`,a,n)})}_start(e){try{if(e.server){let t=e.server,n=t.soupServer;if(!n)throw Error(`options.server has no active Soup.Server. Ensure httpServer.listen() was called before creating WebSocketServer.`);this._server=n,this._setupHandlers(n,e),o();let r=t.address();r&&(this._address={address:r.address,family:r.family,port:r.port}),queueMicrotask(()=>this.emit(`listening`))}else{this._server=new r.Server({}),this._setupHandlers(this._server,e);let t=e.host??`0.0.0.0`,n=e.port;t===`127.0.0.1`||t===`localhost`?this._server.listen_local(n,r.ServerListenOptions.IPV4_ONLY):t===`::1`?this._server.listen_local(n,r.ServerListenOptions.IPV6_ONLY):this._server.listen_all(n,0);let i=this._server.get_listeners(),a=n;if(i&&i.length>0){let e=i[0].get_local_address();e&&typeof e.get_port==`function`&&(a=e.get_port())}o(),this._address={address:t,family:`IPv4`,port:a},queueMicrotask(()=>this.emit(`listening`))}}catch(e){queueMicrotask(()=>this.emit(`error`,e instanceof Error?e:Error(String(e))))}}address(){return this._address}close(e){try{for(let e of this.clients)e.close();this.clients.clear(),this.options.server||this._server?.disconnect(),this._server=null,this._address=null,this.emit(`close`),e&&queueMicrotask(()=>e())}catch(t){let n=t instanceof Error?t:Error(String(t));this.emit(`error`,n),e&&queueMicrotask(()=>e(n))}}handleUpgrade(e,t,n,r){if(!this._validateUpgradeHeaders(e,t))return;let i=e.headers?.[`sec-websocket-key`]??``,doUpgrade=()=>this._completeUpgrade(e,t,i,r);if(this.options.verifyClient){let n=this.options.verifyClient,r=this._buildVerifyClientInfoFromReq(e);if(n.length>=2){n(r,(e,n=401)=>{if(!e){this._abortHandshake(t,n);return}doUpgrade()});return}if(!n(r)){this._abortHandshake(t,401);return}}doUpgrade()}shouldHandle(e){if(this.path===`/`)return!0;let t=e?.url??`/`;return t===this.path||t.startsWith(this.path+`?`)||t.startsWith(this.path+`/`)}_validateUpgradeHeaders(e,t){let n=e.headers??{};if(e.method!==`GET`)return this._abortHandshake(t,405),!1;if((n.upgrade??``).toLowerCase()!==`websocket`||!s.test(n[`sec-websocket-key`]??``))return this._abortHandshake(t,400),!1;let r=Number(n[`sec-websocket-version`]??`0`);return r!==13&&r!==8?(this._abortHandshake(t,426),!1):this.shouldHandle(e)?!0:(this._abortHandshake(t,400),!1)}_completeUpgrade(e,t,a,o){let s=[`HTTP/1.1 101 Switching Protocols`,`Upgrade: websocket`,`Connection: Upgrade`,`Sec-WebSocket-Accept: ${n(`sha1`).update(a+`258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest(`base64`)}`],c=null;if(this.options.handleProtocols){let t=e.headers?.[`sec-websocket-protocol`]??``,n=new Set(t.split(`,`).map(e=>e.trim()).filter(Boolean)),r=this.options.handleProtocols(n,this._buildVerifyClientInfoFromReq(e).req);r&&(c=r,s.push(`Sec-WebSocket-Protocol: ${r}`))}this.emit(`headers`,s,e);let l=s.join(`\r
1
+ import"./_virtual/_rolldown/runtime.js";import"./constants.js";import{closeReason as e}from"./validation.js";import{EventEmitter as t}from"@gjsify/events";import{Buffer as n}from"@gjsify/buffer";import{abortConnection as r,isRefusedEcho as i,isTransportFailure as a,soupCloseCode as o}from"@gjsify/websocket";import{createHash as s}from"@gjsify/crypto";import c from"@girs/soup-3.0";import l from"@girs/glib-2.0";import u from"@girs/gio-2.0";import{createNodeError as d,ensureMainLoop as f}from"@gjsify/utils/core";const p=/^[+/0-9A-Za-z]{22}==$/,m=new Set;var ServerSideWebSocket=class extends t{static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3;CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState=1;protocol=``;extensions=``;url=``;_conn;constructor(e,t){super(),this._conn=e,this.url=t,e.connect(`message`,(e,t,r)=>{let i=r.get_data();if(t===c.WebsocketDataType.TEXT){let e=typeof i==`string`?i:i?new TextDecoder(`utf-8`).decode(i):``;this.emit(`message`,e,!1)}else{let e=i?n.from(i):n.alloc(0);this.emit(`message`,e,!0)}}),e.connect(`closing`,()=>{this.readyState===1&&(this.readyState=2)}),e.connect(`closed`,()=>{this.readyState=3;let t=e.get_close_code()||1006,r=e.get_close_data()||``;this.emit(`close`,t,n.from(r))}),e.connect(`error`,(e,t)=>{a(t)||i(e,t)||this.emit(`error`,Error(t.message))})}send(e,t,r){let i=typeof t==`function`?t:r;if(!this._soupOpen()){if(this.readyState===1&&(this.readyState=2),i){let e=this.readyState===3?`CLOSED`:`CLOSING`,t=Error(`WebSocket is not open: readyState ${this.readyState} (${e})`);queueMicrotask(()=>i(t))}return}try{if(typeof e==`string`){let t=new TextEncoder().encode(e);this._conn.send_message(c.WebsocketDataType.TEXT,new l.Bytes(t))}else{let t;if(n.isBuffer(e)){let n=e;t=new l.Bytes(new Uint8Array(n.buffer,n.byteOffset,n.byteLength))}else if(e instanceof ArrayBuffer)t=new l.Bytes(new Uint8Array(e));else if(ArrayBuffer.isView(e)){let n=e;t=new l.Bytes(new Uint8Array(n.buffer,n.byteOffset,n.byteLength))}else throw TypeError(`Unsupported send() payload type`);this._conn.send_message(c.WebsocketDataType.BINARY,t)}i&&queueMicrotask(()=>i())}catch(e){let t=e instanceof Error?e:Error(String(e));queueMicrotask(i?()=>i(t):()=>this.emit(`error`,t))}}close(t,n){if(this.readyState===3||this.readyState===2)return;let r=e(t,n);if(this.readyState=2,this._soupOpen())try{this._conn.close(o(this._conn,t??1e3),r??null)}catch(e){this.emit(`error`,e instanceof Error?e:Error(String(e)))}}terminate(){this.readyState!==3&&(this.readyState=2,r(this._conn))}_soupOpen(){return this._conn.get_state()===c.WebsocketState.OPEN}},WebSocketServer=class extends t{options;clients=new Set;path;_server=null;_address=null;constructor(e={},t){if(super(),this.options=e,this.path=e.path??`/`,e.noServer){if(e.port!==void 0||e.server!==void 0)throw Error(`ws.WebSocketServer: { noServer: true } is mutually exclusive with port and server.`);t&&this.once(`listening`,t);return}if(e.port===void 0&&!e.server)throw Error(`ws.WebSocketServer requires either options.port or options.server on Gjs.`);t&&this.once(`listening`,t),this._start(e)}_buildVerifyClientInfo(e){let t=e.get_request_headers(),n={};t.foreach((e,t)=>{let r=e.toLowerCase(),i=n[r];i===void 0?n[r]=t:Array.isArray(i)?i.push(t):n[r]=[i,t]});let r=e.get_uri(),i=r.get_path()??`/`,a=r.get_query(),o=a?`${i}?${a}`:i,s=e.get_remote_host()??`127.0.0.1`,c=e.get_remote_address(),l=c instanceof u.InetSocketAddress?c.get_port():0;return{origin:n.origin??``,secure:!1,req:{method:e.get_method(),url:o,headers:n,socket:{remoteAddress:s,remotePort:l}}}}_setupHandlers(e,t){try{e.remove_websocket_extension(c.WebsocketExtensionDeflate.$gtype)}catch{}if(t.verifyClient){let n=t.verifyClient;e.add_handler(this.path,(e,t)=>{if((t.get_request_headers().get_one(`Upgrade`)??``).toLowerCase()!==`websocket`)return;let r=this._buildVerifyClientInfo(t);n.length>=2?(t.pause(),n(r,(e,n=401)=>{e||t.set_status(n,null),t.unpause()})):n(r)||t.set_status(401,null)})}e.add_websocket_handler(this.path,null,null,(e,n,r,i)=>{let a=new ServerSideWebSocket(i,n.get_uri()?.to_string()??this.path),o=this._buildVerifyClientInfo(n).req;if(t.handleProtocols){let e=n.get_request_headers().get_one(`Sec-WebSocket-Protocol`)??``,r=new Set(e.split(`,`).map(e=>e.trim()).filter(Boolean)),i=t.handleProtocols(r,o);i&&(a.protocol=i)}t.clientTracking!==!1&&(this.clients.add(a),a.on(`close`,()=>this.clients.delete(a))),this.emit(`connection`,a,o)})}_start(e){try{if(e.server){let t=e.server,n=t.soupServer;if(!n)throw Error(`options.server has no active Soup.Server. Ensure httpServer.listen() was called before creating WebSocketServer.`);this._server=n,this._setupHandlers(n,e),f();let r=t.address();r&&(this._address={address:r.address,family:r.family,port:r.port}),m.add(this),queueMicrotask(()=>this.emit(`listening`))}else{this._server=new c.Server({}),this._setupHandlers(this._server,e);let t=e.host??`0.0.0.0`,n=e.port;t===`127.0.0.1`||t===`localhost`?this._server.listen_local(n,c.ServerListenOptions.IPV4_ONLY):t===`::1`?this._server.listen_local(n,c.ServerListenOptions.IPV6_ONLY):this._server.listen_all(n,0);let r=this._server.get_listeners(),i=n;if(r&&r.length>0){let e=r[0].get_local_address();e&&typeof e.get_port==`function`&&(i=e.get_port())}f(),this._address={address:t,family:`IPv4`,port:i},m.add(this),queueMicrotask(()=>this.emit(`listening`))}}catch(t){let n=d(t,`listen`,{address:e.host,port:e.port});queueMicrotask(()=>this.emit(`error`,n))}}address(){return this._address}close(e){try{m.delete(this);for(let e of this.clients)e.close();this.clients.clear(),this.options.server||this._server?.disconnect(),this._server=null,this._address=null,this.emit(`close`),e&&queueMicrotask(()=>e())}catch(t){let n=t instanceof Error?t:Error(String(t));this.emit(`error`,n),e&&queueMicrotask(()=>e(n))}}handleUpgrade(e,t,n,r){if(!this._validateUpgradeHeaders(e,t))return;let i=e.headers?.[`sec-websocket-key`]??``,doUpgrade=()=>this._completeUpgrade(e,t,i,r);if(this.options.verifyClient){let n=this.options.verifyClient,r=this._buildVerifyClientInfoFromReq(e);if(n.length>=2){n(r,(e,n=401)=>{if(!e){this._abortHandshake(t,n);return}doUpgrade()});return}if(!n(r)){this._abortHandshake(t,401);return}}doUpgrade()}shouldHandle(e){if(this.path===`/`)return!0;let t=e?.url??`/`;return t===this.path||t.startsWith(this.path+`?`)||t.startsWith(this.path+`/`)}_validateUpgradeHeaders(e,t){let n=e.headers??{};if(e.method!==`GET`)return this._abortHandshake(t,405),!1;if((n.upgrade??``).toLowerCase()!==`websocket`||!p.test(n[`sec-websocket-key`]??``))return this._abortHandshake(t,400),!1;let r=Number(n[`sec-websocket-version`]??`0`);return r!==13&&r!==8?(this._abortHandshake(t,426),!1):this.shouldHandle(e)?!0:(this._abortHandshake(t,400),!1)}_completeUpgrade(e,t,n,r){let i=[`HTTP/1.1 101 Switching Protocols`,`Upgrade: websocket`,`Connection: Upgrade`,`Sec-WebSocket-Accept: ${s(`sha1`).update(n+`258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest(`base64`)}`],a=null;if(this.options.handleProtocols){let t=e.headers?.[`sec-websocket-protocol`]??``,n=new Set(t.split(`,`).map(e=>e.trim()).filter(Boolean)),r=this.options.handleProtocols(n,this._buildVerifyClientInfoFromReq(e).req);r&&(a=r,i.push(`Sec-WebSocket-Protocol: ${r}`))}this.emit(`headers`,i,e);let o=i.join(`\r
2
2
  `)+`\r
3
3
  \r
4
- `;t.write(l,()=>{let n=typeof t._releaseIOStream==`function`?t._releaseIOStream():null;if(!n){t.destroy?.();return}let a=e.url??`/`,s=i.Uri.parse(`ws://localhost${a}`,i.UriFlags.NONE),l=new ServerSideWebSocket(r.WebsocketConnection.new(n,s,r.WebsocketConnectionType.SERVER,null,c,[]),a);c&&(l.protocol=c),this.options.clientTracking!==!1&&(this.clients.add(l),l.on(`close`,()=>this.clients.delete(l))),o(l,e)})}_abortHandshake(e,t){let n={400:`Bad Request`,401:`Unauthorized`,403:`Forbidden`,405:`Method Not Allowed`,426:`Upgrade Required`}[t]??`Error`;e.write?.(`HTTP/1.1 ${t} ${n}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`,()=>{e.destroy?.()})}_buildVerifyClientInfoFromReq(e){let t=e.headers??{};return{origin:t.origin??``,secure:!1,req:{method:e.method??`GET`,url:e.url??`/`,headers:t,socket:{remoteAddress:e.socket?.remoteAddress??`127.0.0.1`,remotePort:e.socket?.remotePort??0}}}}};export{WebSocketServer};
4
+ `;t.write(o,()=>{let n=typeof t._releaseIOStream==`function`?t._releaseIOStream():null;if(!n){t.destroy?.();return}let i=e.url??`/`,o=l.Uri.parse(`ws://localhost${i}`,l.UriFlags.NONE),s=new ServerSideWebSocket(c.WebsocketConnection.new(n,o,c.WebsocketConnectionType.SERVER,null,a,[]),i);a&&(s.protocol=a),this.options.clientTracking!==!1&&(this.clients.add(s),s.on(`close`,()=>this.clients.delete(s))),r(s,e)})}_abortHandshake(e,t){let n={400:`Bad Request`,401:`Unauthorized`,403:`Forbidden`,405:`Method Not Allowed`,426:`Upgrade Required`}[t]??`Error`;e.write?.(`HTTP/1.1 ${t} ${n}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`,()=>{e.destroy?.()})}_buildVerifyClientInfoFromReq(e){let t=e.headers??{};return{origin:t.origin??``,secure:!1,req:{method:e.method??`GET`,url:e.url??`/`,headers:t,socket:{remoteAddress:e.socket?.remoteAddress??`127.0.0.1`,remotePort:e.socket?.remotePort??0}}}}};export{WebSocketServer};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{BINARY_TYPES as e}from"./constants.js";import{EventEmitter as t}from"@gjsify/events";import{Buffer as n}from"@gjsify/buffer";import{WebSocket as r}from"@gjsify/websocket";var WebSocket=class extends t{static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3;CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState=0;url=``;protocol=``;extensions=``;bufferedAmount=0;binaryType=`nodebuffer`;_native=null;constructor(e,t,n={}){if(super(),e===null){queueMicrotask(()=>this._fail(`Constructing ws.WebSocket with null address is not supported on Gjs`));return}this.url=typeof e==`string`?e:String(e);let r=this._resolveProtocols(t,n);this._openNative(this.url,r,n)}_resolveProtocols(e,t){if(e!==void 0)return Array.isArray(e)?e:[e];if(t.protocols!==void 0)return Array.isArray(t.protocols)?t.protocols:[t.protocols];if(t.protocol!==void 0)return[t.protocol]}_openNative(e,t,n){if(typeof r!=`function`){queueMicrotask(()=>this._fail("@gjsify/websocket provided no WebSocket constructor. On Node.js 22+ globalThis.WebSocket is native; on older Node install `ws` directly, or ensure globalThis.WebSocket is set before @gjsify/ws is imported."));return}let i={perMessageDeflate:n.perMessageDeflate!==!1,headers:n.headers,origin:n.origin,handshakeTimeout:n.handshakeTimeout};try{this._native=new r(e,t,i)}catch(e){queueMicrotask(()=>this._fail(e instanceof Error?e:Error(String(e))));return}this._native.binaryType=`arraybuffer`,this._native.addEventListener(`open`,()=>this._onOpen()),this._native.addEventListener(`message`,e=>this._onMessage(e)),this._native.addEventListener(`close`,e=>this._onClose(e)),this._native.addEventListener(`error`,e=>this._onError(e))}_fail(e){let t=typeof e==`string`?Error(e):e;this.readyState=3,this.emit(`error`,t),this._dispatchEvent(`error`,{error:t,message:t.message}),this.emit(`close`,1006,n.from(t.message)),this._dispatchEvent(`close`,{code:1006,reason:t.message,wasClean:!1})}_onOpen(){this.readyState=1,typeof this._native?.protocol==`string`&&(this.protocol=this._native.protocol),typeof this._native?.extensions==`string`&&(this.extensions=this._native.extensions),this.emit(`open`),this._dispatchEvent(`open`,{})}_onMessage(e){let t=e?.data,n,r=!1;typeof t==`string`?(n=t,r=!1):t instanceof ArrayBuffer?(r=!0,n=this._decodeBinary(t)):ArrayBuffer.isView(t)?(r=!0,n=this._decodeBinary(t.buffer)):(n=t,r=!1),this.emit(`message`,n,r),this._dispatchEvent(`message`,{data:n,type:r?`binary`:`text`})}_decodeBinary(e){switch(this.binaryType){case`arraybuffer`:return e;case`fragments`:return[n.from(e)];case`blob`:{let t=globalThis.Blob;return t?new t([new Uint8Array(e)]):n.from(e)}default:return n.from(e)}}_onClose(e){let t=typeof e?.code==`number`?e.code:1006,r=typeof e?.reason==`string`?e.reason:``;this.readyState=3,this.emit(`close`,t,n.from(r)),this._dispatchEvent(`close`,{code:t,reason:r,wasClean:!!e?.wasClean})}_onError(e){let t=typeof e?.message==`string`&&e.message||`WebSocket error`,n=e?.error instanceof Error?e.error:Error(t);this.emit(`error`,n),this._dispatchEvent(`error`,{error:n,message:t})}send(e,t,n){let r=typeof t==`function`?t:n;if(this.readyState===0)throw Error(`WebSocket is not open: readyState 0 (CONNECTING)`);if(this.readyState!==1){let e=Error(`WebSocket is not open: readyState `+this.readyState);queueMicrotask(r?()=>r(e):()=>this.emit(`error`,e));return}this._nativeSend(e,r)}_nativeSend(e,t){try{let r;r=typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):n.isBuffer(e)?e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength):e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e:String(e),this._native?.send(r),t&&queueMicrotask(()=>t())}catch(e){let n=e instanceof Error?e:Error(String(e));queueMicrotask(t?()=>t(n):()=>this.emit(`error`,n))}}_sizeOf(e){return typeof e==`string`?e.length:e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e.byteLength:0}close(e,t){if(this.readyState!==3&&this.readyState!==2){this.readyState=2;try{let r=t===void 0?void 0:n.isBuffer(t)?t.toString(`utf8`):String(t);e===void 0?this._native?.close():r===void 0?this._native?.close(e):this._native?.close(e,r)}catch(e){this.emit(`error`,e instanceof Error?e:Error(String(e)))}}}terminate(){if(this.readyState!==3){this.readyState=2;try{this._native?.close(1006,`terminated`)}catch{}}}get isPaused(){return this.readyState===2||this.readyState===3}_eventTargetListeners=new Map;addEventListener(e,t){let n=this._eventTargetListeners.get(e);n||(n=new Set,this._eventTargetListeners.set(e,n)),n.add(t)}removeEventListener(e,t){this._eventTargetListeners.get(e)?.delete(t)}_dispatchEvent(e,t){let n=this._eventTargetListeners.get(e);if(!n||n.size===0)return;let r=Object.assign({type:e,target:this},t);for(let e of n)try{e(r)}catch(e){queueMicrotask(()=>this.emit(`error`,e))}}static get BINARY_TYPES(){return e}};WebSocket.WebSocket=WebSocket;export{WebSocket};
1
+ import"./_virtual/_rolldown/runtime.js";import{BINARY_TYPES as e}from"./constants.js";import{closeReason as t}from"./validation.js";import{EventEmitter as n}from"@gjsify/events";import{Buffer as r}from"@gjsify/buffer";import{WebSocket as i,isTransportFailure as a,kClose as o}from"@gjsify/websocket";var WebSocket=class extends n{static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3;CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState=0;url=``;protocol=``;extensions=``;bufferedAmount=0;binaryType=`nodebuffer`;_native=null;constructor(e,t,n={}){if(super(),e===null){queueMicrotask(()=>this._fail(`Constructing ws.WebSocket with null address is not supported on Gjs`));return}this.url=typeof e==`string`?e:String(e);let r=n,i;typeof t==`string`||Array.isArray(t)?i=t:t!=null&&(r=t);let a=this._resolveProtocols(i,r);this._openNative(this.url,a,r)}_resolveProtocols(e,t){if(e!==void 0)return Array.isArray(e)?e:[e];if(t.protocols!==void 0)return Array.isArray(t.protocols)?t.protocols:[t.protocols];if(t.protocol!==void 0)return[t.protocol]}_openNative(e,t,n){if(typeof i!=`function`){queueMicrotask(()=>this._fail("@gjsify/websocket provided no WebSocket constructor. On Node.js 22+ globalThis.WebSocket is native; on older Node install `ws` directly, or ensure globalThis.WebSocket is set before @gjsify/ws is imported."));return}let r={perMessageDeflate:n.perMessageDeflate!==!1,headers:n.headers,origin:n.origin,handshakeTimeout:n.handshakeTimeout,rejectUnauthorized:n.rejectUnauthorized};try{this._native=new i(e,t,r)}catch(e){queueMicrotask(()=>this._fail(e instanceof Error?e:Error(String(e))));return}this._native.binaryType=`arraybuffer`,this._native.addEventListener(`open`,()=>this._onOpen()),this._native.addEventListener(`message`,e=>this._onMessage(e)),this._native.addEventListener(`close`,e=>this._onClose(e)),this._native.addEventListener(`error`,e=>this._onError(e))}_fail(e){let t=typeof e==`string`?Error(e):e;this.readyState=3,this.emit(`error`,t),this._dispatchEvent(`error`,{error:t,message:t.message}),this.emit(`close`,1006,r.from(t.message)),this._dispatchEvent(`close`,{code:1006,reason:t.message,wasClean:!1})}_onOpen(){this.readyState=1,typeof this._native?.protocol==`string`&&(this.protocol=this._native.protocol),typeof this._native?.extensions==`string`&&(this.extensions=this._native.extensions),this.emit(`open`),this._dispatchEvent(`open`,{})}_onMessage(e){let t=e?.data,n,r=!1;typeof t==`string`?(n=t,r=!1):t instanceof ArrayBuffer?(r=!0,n=this._decodeBinary(t)):ArrayBuffer.isView(t)?(r=!0,n=this._decodeBinary(t.buffer)):(n=t,r=!1),this.emit(`message`,n,r),this._dispatchEvent(`message`,{data:n,type:r?`binary`:`text`})}_decodeBinary(e){switch(this.binaryType){case`arraybuffer`:return e;case`fragments`:return[r.from(e)];case`blob`:{let t=globalThis.Blob;return t?new t([new Uint8Array(e)]):r.from(e)}default:return r.from(e)}}_onClose(e){let t=typeof e?.code==`number`?e.code:1006,n=typeof e?.reason==`string`?e.reason:``;this.readyState=3,this.emit(`close`,t,r.from(n)),this._dispatchEvent(`close`,{code:t,reason:n,wasClean:!!e?.wasClean})}_onError(e){if(e?.error instanceof Error&&a(e.error.cause))return;let t=typeof e?.message==`string`&&e.message||`WebSocket error`,n=e?.error instanceof Error?e.error:Error(t);this.emit(`error`,n),this._dispatchEvent(`error`,{error:n,message:t})}send(e,t,n){let r=typeof t==`function`?t:n;if(this.readyState===0)throw Error(`WebSocket is not open: readyState 0 (CONNECTING)`);if(this.readyState===1&&this._native?.readyState===2&&(this.readyState=2),this.readyState!==1){let e=Error(`WebSocket is not open: readyState `+this.readyState);queueMicrotask(r?()=>r(e):()=>this.emit(`error`,e));return}this._nativeSend(e,r)}_nativeSend(e,t){try{let n;n=typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):r.isBuffer(e)?e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength):e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e:String(e),this._native?.send(n),t&&queueMicrotask(()=>t())}catch(e){let n=e instanceof Error?e:Error(String(e));queueMicrotask(t?()=>t(n):()=>this.emit(`error`,n))}}_sizeOf(e){return typeof e==`string`?e.length:e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e.byteLength:0}close(e,n){if(this.readyState===3)return;if(this.readyState===0){this.readyState=2,this._nativeClose();return}if(this.readyState===2)return;let r=t(e,n);this.readyState=2;try{this._nativeClose(e,r)}catch(e){this.emit(`error`,e instanceof Error?e:Error(String(e)))}}_nativeClose(e,t){let n=this._native;n&&(n[o]?n[o](e,t):e===void 0?n.close():t===void 0?n.close(e):n.close(e,t))}terminate(){if(this.readyState===3)return;this.readyState=2;let e=this._native;e&&(e[o]?e[o](1006):e.close())}get isPaused(){return this.readyState===2||this.readyState===3}_eventTargetListeners=new Map;addEventListener(e,t){let n=this._eventTargetListeners.get(e);n||(n=new Set,this._eventTargetListeners.set(e,n)),n.add(t)}removeEventListener(e,t){this._eventTargetListeners.get(e)?.delete(t)}_dispatchEvent(e,t){let n=this._eventTargetListeners.get(e);if(!n||n.size===0)return;let r=Object.assign({type:e,target:this},t);for(let e of n)try{e(r)}catch(e){queueMicrotask(()=>this.emit(`error`,e))}}static get BINARY_TYPES(){return e}};WebSocket.WebSocket=WebSocket;export{WebSocket};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { Buffer } from '@gjsify/buffer';
2
+ /** The status codes ws lets an endpoint send: RFC 6455's defined codes minus
3
+ * the three that must never go on the wire (1004 reserved, 1005/1006
4
+ * local-only), plus the registered and private ranges. */
5
+ export declare function isValidStatusCode(code: number): boolean;
6
+ /** Validate close(code, reason) the way ws does before anything is sent, and
7
+ * return the reason as a string. ws throws these to the caller of close();
8
+ * Soup would instead log a CRITICAL for 1005/1006 and send nothing. */
9
+ export declare function closeReason(code: unknown, reason: string | Buffer | undefined): string | undefined;
@@ -96,6 +96,7 @@ declare class ServerSideWebSocket extends EventEmitter {
96
96
  send(data: string | Buffer | ArrayBuffer | ArrayBufferView, optionsOrCb?: ((err?: Error) => void) | object, cb?: (err?: Error) => void): void;
97
97
  close(code?: number, reason?: string | Buffer): void;
98
98
  terminate(): void;
99
+ private _soupOpen;
99
100
  }
100
101
  /** `ws.WebSocketServer` — listens on a TCP port (or attaches to an existing
101
102
  * @gjsify/http Server) and emits 'connection' events wrapping
@@ -25,6 +25,9 @@ export interface ClientOptions {
25
25
  origin?: string;
26
26
  headers?: Record<string, string | string[]>;
27
27
  handshakeTimeout?: number;
28
+ /** `false` accepts a self-signed or otherwise unverifiable server
29
+ * certificate on `wss:`. */
30
+ rejectUnauthorized?: boolean;
28
31
  /** Enable permessage-deflate (RFC 7692). Defaults to true, matching the real
29
32
  * ws npm package. Set to false to disable deflate negotiation (useful when
30
33
  * the remote server has buggy deflate handling). */
@@ -77,6 +80,7 @@ export declare class WebSocket extends EventEmitter {
77
80
  * because the W3C ambient type comes from multiple realms depending on where
78
81
  * this bundle ends up (GJS browser-like globals vs. Node's undici). */
79
82
  private _native;
83
+ constructor(address: string | URL | null, options?: ClientOptions);
80
84
  constructor(address: string | URL | null, protocols?: string | string[], options?: ClientOptions);
81
85
  /** Merge `protocols` arg and `options.protocols` / `options.protocol`. */
82
86
  private _resolveProtocols;
@@ -99,9 +103,14 @@ export declare class WebSocket extends EventEmitter {
99
103
  private _nativeSend;
100
104
  private _sizeOf;
101
105
  close(code?: number, reason?: string | Buffer): void;
102
- /** ws-only: force-close without sending a Close frame. On Gjs we can't bypass
103
- * Soup's close handshake, so terminate is approximated as close(1006).
104
- * Known gap vs. ws semantics — documented. */
106
+ /** The W3C close() rejects 1001–1014, which ws sends; @gjsify/websocket
107
+ * takes them through its internal hook. A host's own WebSocket has no
108
+ * hook and keeps its W3C rules. */
109
+ private _nativeClose;
110
+ /** ws-only: drop the connection without a Close frame; 'close' follows
111
+ * with 1006. The W3C close() cannot express that (it rejects 1006);
112
+ * @gjsify/websocket's close hook reads 1006 — RFC 6455's code for a
113
+ * connection closed without a Close frame — as exactly this. */
105
114
  terminate(): void;
106
115
  /** Convenience: returns true if the socket is closed or closing. Matches
107
116
  * ws.isPaused() / internal state checks that some consumers rely on. */
@@ -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/ws",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Drop-in replacement for the `ws` npm package on Gjs — wraps globalThis.WebSocket (Soup.WebsocketConnection) and Soup.Server for the WebSocketServer side",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -41,15 +41,16 @@
41
41
  "@girs/gio-2.0": "5.4.0",
42
42
  "@girs/glib-2.0": "5.4.0",
43
43
  "@girs/soup-3.0": "5.4.0",
44
- "@gjsify/buffer": "^0.52.0",
45
- "@gjsify/crypto": "^0.52.0",
46
- "@gjsify/events": "^0.52.0",
47
- "@gjsify/utils": "^0.52.0",
48
- "@gjsify/websocket": "^0.52.0"
44
+ "@gjsify/buffer": "^0.53.0",
45
+ "@gjsify/crypto": "^0.53.0",
46
+ "@gjsify/events": "^0.53.0",
47
+ "@gjsify/utils": "^0.53.0",
48
+ "@gjsify/websocket": "^0.53.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@gjsify/cli": "^0.52.0",
52
- "@gjsify/unit": "^0.52.0",
51
+ "@gjsify/cli": "^0.53.0",
52
+ "@gjsify/runtime": "^0.53.0",
53
+ "@gjsify/unit": "^0.53.0",
53
54
  "@types/node": "^25.9.2",
54
55
  "typescript": "^6.0.3"
55
56
  },