@gjsify/ws 0.3.21 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/stream.js +1 -1
- package/lib/esm/websocket-server.js +1 -1
- package/lib/esm/websocket.js +1 -1
- package/package.json +50 -47
- package/src/constants.ts +0 -15
- package/src/index.spec.ts +0 -144
- package/src/index.ts +0 -25
- package/src/stream.spec.ts +0 -121
- package/src/stream.ts +0 -107
- package/src/test.mts +0 -5
- package/src/websocket-server.spec.ts +0 -354
- package/src/websocket-server.ts +0 -561
- package/src/websocket.ts +0 -391
- package/tsconfig.json +0 -29
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
package/lib/esm/stream.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Duplex as e}from"node:stream";function emitClose(e){e.emit(`close`)}function duplexOnEnd(){!this.destroyed&&this._writableState.finished&&this.destroy()}function duplexOnError(e){this.removeListener(`error`,duplexOnError),this.destroy(),this.listenerCount(`error`)===0&&this.emit(`error`,e)}function createWebSocketStream(t,n={}){let r=!0,i=new e({...n,autoDestroy:!1,emitClose:!1,objectMode:!1,writableObjectMode:!1});return t.on(`message`,(e,n)=>{let r;r=n||i.readableObjectMode?e:typeof e==`string`?Buffer.from(e):e,!i.push(r)&&typeof t.pause==`function`&&t.pause()}),t.once(`error`,e=>{i.destroyed||(r=!1,i.destroy(e))}),t.once(`close`,()=>{i.destroyed||i.push(null)}),i._destroy=function(e,n){if(t.readyState===t.CLOSED){n(e),process.nextTick(emitClose,i);return}let a=!1;t.once(`error`,e=>{a=!0,n(e)}),t.once(`close`,()=>{a||n(e),process.nextTick(emitClose,i)}),r&&t.terminate()},i._final=function(e){if(t.readyState===t.CONNECTING){t.once(`open`,()=>i._final(e));return}if(t.readyState===t.CLOSED||t.readyState===t.CLOSING){e();return}t.once(`close`,e),t.close()},i._read=function(){typeof t.resume==`function`&&t.resume()},i._write=function(e,n,r){if(t.readyState===t.CONNECTING){t.once(`open`,()=>i._write(e,n,r));return}t.send(e,r)},i.on(`end`,duplexOnEnd),i.on(`error`,duplexOnError),i}export{createWebSocketStream};
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{Duplex as e}from"node:stream";function emitClose(e){e.emit(`close`)}function duplexOnEnd(){!this.destroyed&&this._writableState.finished&&this.destroy()}function duplexOnError(e){this.removeListener(`error`,duplexOnError),this.destroy(),this.listenerCount(`error`)===0&&this.emit(`error`,e)}function createWebSocketStream(t,n={}){let r=!0,i=new e({...n,autoDestroy:!1,emitClose:!1,objectMode:!1,writableObjectMode:!1});return t.on(`message`,(e,n)=>{let r;r=n||i.readableObjectMode?e:typeof e==`string`?Buffer.from(e):e,!i.push(r)&&typeof t.pause==`function`&&t.pause()}),t.once(`error`,e=>{i.destroyed||(r=!1,i.destroy(e))}),t.once(`close`,()=>{i.destroyed||i.push(null)}),i._destroy=function(e,n){if(t.readyState===t.CLOSED){n(e),process.nextTick(emitClose,i);return}let a=!1;t.once(`error`,e=>{a=!0,n(e)}),t.once(`close`,()=>{a||n(e),process.nextTick(emitClose,i)}),r&&t.terminate()},i._final=function(e){if(t.readyState===t.CONNECTING){t.once(`open`,()=>i._final(e));return}if(t.readyState===t.CLOSED||t.readyState===t.CLOSING){e();return}t.once(`close`,e),t.close()},i._read=function(){typeof t.resume==`function`&&t.resume()},i._write=function(e,n,r){if(t.readyState===t.CONNECTING){t.once(`open`,()=>i._write(e,n,r));return}t.send(e,r)},i.on(`end`,duplexOnEnd),i.on(`error`,duplexOnError),i}export{createWebSocketStream};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
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";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(){if(this.readyState!==3){this.readyState=2;try{this._conn.close(1006,null)}catch{}}}},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){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{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";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(){if(this.readyState!==3){this.readyState=2;try{this._conn.close(1006,null)}catch{}}}},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){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
|
|
2
2
|
`)+`\r
|
|
3
3
|
\r
|
|
4
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};
|
package/lib/esm/websocket.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
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=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=e;if(typeof e==`number`||typeof e==`boolean`)r=String(e);else if(n.isBuffer(e)){let t=e;r=t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength)}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{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=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=e;if(typeof e==`number`||typeof e==`boolean`)r=String(e);else if(n.isBuffer(e)){let t=e;r=t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength)}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};
|
package/package.json
CHANGED
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"name": "@gjsify/ws",
|
|
3
|
+
"version": "0.4.3",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"module": "lib/esm/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/esm/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
|
|
19
|
+
"check": "tsc --noEmit",
|
|
20
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
21
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
22
|
+
"build:types": "tsc",
|
|
23
|
+
"build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
|
|
24
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
25
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
26
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
|
|
27
|
+
"test:gjs": "gjsify run test.gjs.mjs",
|
|
28
|
+
"test:node": "node test.node.mjs"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"gjs",
|
|
32
|
+
"node",
|
|
33
|
+
"ws",
|
|
34
|
+
"websocket"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
|
|
38
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
|
|
39
|
+
"@girs/soup-3.0": "3.6.6-4.0.0-rc.15",
|
|
40
|
+
"@gjsify/buffer": "workspace:^",
|
|
41
|
+
"@gjsify/crypto": "workspace:^",
|
|
42
|
+
"@gjsify/events": "workspace:^",
|
|
43
|
+
"@gjsify/utils": "workspace:^",
|
|
44
|
+
"@gjsify/websocket": "workspace:^"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@gjsify/cli": "workspace:^",
|
|
48
|
+
"@gjsify/unit": "workspace:^",
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
12
51
|
}
|
|
13
|
-
|
|
14
|
-
"scripts": {
|
|
15
|
-
"clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
|
|
16
|
-
"check": "tsc --noEmit",
|
|
17
|
-
"build": "yarn build:gjsify && yarn build:types",
|
|
18
|
-
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
19
|
-
"build:types": "tsc",
|
|
20
|
-
"build:test": "yarn build:test:gjs && yarn build:test:node",
|
|
21
|
-
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
22
|
-
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
23
|
-
"test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
|
|
24
|
-
"test:gjs": "gjsify run test.gjs.mjs",
|
|
25
|
-
"test:node": "node test.node.mjs"
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"gjs",
|
|
29
|
-
"node",
|
|
30
|
-
"ws",
|
|
31
|
-
"websocket"
|
|
32
|
-
],
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@girs/gio-2.0": "2.88.0-4.0.0-rc.14",
|
|
35
|
-
"@girs/glib-2.0": "2.88.0-4.0.0-rc.14",
|
|
36
|
-
"@girs/soup-3.0": "3.6.6-4.0.0-rc.14",
|
|
37
|
-
"@gjsify/buffer": "^0.3.21",
|
|
38
|
-
"@gjsify/crypto": "^0.3.21",
|
|
39
|
-
"@gjsify/events": "^0.3.21",
|
|
40
|
-
"@gjsify/utils": "^0.3.21",
|
|
41
|
-
"@gjsify/websocket": "^0.3.21"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@gjsify/cli": "^0.3.21",
|
|
45
|
-
"@gjsify/unit": "^0.3.21",
|
|
46
|
-
"@types/node": "^25.6.2",
|
|
47
|
-
"typescript": "^6.0.3"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
52
|
+
}
|
package/src/constants.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Constants shared between WebSocket and WebSocketServer.
|
|
2
|
-
// Values chosen to match the `ws` npm package where observable.
|
|
3
|
-
|
|
4
|
-
export const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'] as const;
|
|
5
|
-
export const EMPTY_BUFFER = new Uint8Array(0);
|
|
6
|
-
|
|
7
|
-
// WebSocket readyState values — identical to the W3C spec and `ws` npm pkg.
|
|
8
|
-
export const CONNECTING = 0;
|
|
9
|
-
export const OPEN = 1;
|
|
10
|
-
export const CLOSING = 2;
|
|
11
|
-
export const CLOSED = 3;
|
|
12
|
-
|
|
13
|
-
/** Internal marker for native `globalThis.WebSocket` instances handed in via
|
|
14
|
-
* `{ socket }` option (not currently supported but reserved). */
|
|
15
|
-
export const kWebSocket = Symbol('ws:kWebSocket');
|
package/src/index.spec.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Ported from refs/ws/test/websocket.test.js and websocket-server.test.js
|
|
3
|
-
// Original: Copyright (c) 2011+ Einar Otto Stangvik. MIT.
|
|
4
|
-
// Rewritten for @gjsify/unit — behavior preserved, scope narrowed to the
|
|
5
|
-
// surface that @gjsify/ws implements on top of Soup.WebsocketConnection.
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, on } from '@gjsify/unit';
|
|
8
|
-
// `ws` resolves to the real npm package on Node (validates the ws-API
|
|
9
|
-
// surface our wrapper targets) and to @gjsify/ws on GJS (via the alias
|
|
10
|
-
// in @gjsify/resolve-npm) — so the same spec exercises both.
|
|
11
|
-
// @ts-ignore — @types/ws declares WebSocket as the default export in some
|
|
12
|
-
// versions and not in others. The runtime is correct on both paths
|
|
13
|
-
// (the npm package does `module.exports = WebSocket` in CJS land).
|
|
14
|
-
import ws, { WebSocket, WebSocketServer } from 'ws';
|
|
15
|
-
|
|
16
|
-
/** Construct a WebSocket that attempts to connect to a non-routable address.
|
|
17
|
-
* We silence 'error' so the inevitable connection failure (after close())
|
|
18
|
-
* doesn't show up as an unhandled rejection in the test output. Tests that
|
|
19
|
-
* specifically care about errors re-attach a listener. */
|
|
20
|
-
function makeDeadSocket(): WebSocket {
|
|
21
|
-
const s = new WebSocket('ws://example.invalid:1');
|
|
22
|
-
s.on('error', () => {});
|
|
23
|
-
return s;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default async () => {
|
|
27
|
-
await describe('@gjsify/ws module exports', async () => {
|
|
28
|
-
await it('default export is the WebSocket class', async () => {
|
|
29
|
-
expect(typeof ws).toBe('function');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
await it('named WebSocket export equals the default export', async () => {
|
|
33
|
-
expect(ws).toBe(WebSocket);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// ws's own index.js runs `WebSocket.WebSocket = WebSocket` etc. at load.
|
|
37
|
-
// Real npm ws exposes these via CJS but Node's ESM↔CJS bridge does NOT
|
|
38
|
-
// surface post-load property assignments as static named exports, so on
|
|
39
|
-
// Node these appear as `undefined` on the default export. Our @gjsify/ws
|
|
40
|
-
// is authored as ESM from the start and preserves them, so we assert the
|
|
41
|
-
// self-references on GJS where the wrapper is actually loaded.
|
|
42
|
-
await on('Gjs', async () => {
|
|
43
|
-
await it('GJS: WebSocket.WebSocket self-reference matches ws-npm pattern', async () => {
|
|
44
|
-
expect((WebSocket as any).WebSocket).toBe(WebSocket);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
await it('GJS: WebSocket.Server is an alias for WebSocketServer', async () => {
|
|
48
|
-
expect((WebSocket as any).Server).toBe(WebSocketServer);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await it('typeof ws === "function" satisfies simple-websocket heuristic', async () => {
|
|
53
|
-
// simple-websocket does:
|
|
54
|
-
// const _WebSocket = typeof ws !== 'function' ? globalThis.WebSocket : ws
|
|
55
|
-
// Our drop-in must land on the `ws` branch so consumer sees a real class.
|
|
56
|
-
expect(typeof ws).toBe('function');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await describe('WebSocket constants', async () => {
|
|
61
|
-
await it('exposes readyState constants on the class', async () => {
|
|
62
|
-
expect(WebSocket.CONNECTING).toBe(0);
|
|
63
|
-
expect(WebSocket.OPEN).toBe(1);
|
|
64
|
-
expect(WebSocket.CLOSING).toBe(2);
|
|
65
|
-
expect(WebSocket.CLOSED).toBe(3);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await it('exposes readyState constants on instances', async () => {
|
|
69
|
-
// Construct with an unusable URL so we don't trigger a real connection
|
|
70
|
-
// during a unit test; instance properties are set in the constructor
|
|
71
|
-
// independently of connection state.
|
|
72
|
-
const s = new WebSocket('ws://localhost:1');
|
|
73
|
-
expect(s.CONNECTING).toBe(0);
|
|
74
|
-
expect(s.OPEN).toBe(1);
|
|
75
|
-
expect(s.CLOSING).toBe(2);
|
|
76
|
-
expect(s.CLOSED).toBe(3);
|
|
77
|
-
s.close();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await describe('WebSocket construction', async () => {
|
|
82
|
-
await it('stores the url string', async () => {
|
|
83
|
-
const s = new WebSocket('ws://example.invalid:1/path');
|
|
84
|
-
expect(s.url).toBe('ws://example.invalid:1/path');
|
|
85
|
-
s.close();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
await it('stores the url from a URL object', async () => {
|
|
89
|
-
const url = new URL('ws://example.invalid:1/u');
|
|
90
|
-
const s = new WebSocket(url);
|
|
91
|
-
expect(s.url).toContain('ws://example.invalid:1');
|
|
92
|
-
s.close();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
await it('starts in CONNECTING state', async () => {
|
|
96
|
-
const s = makeDeadSocket();
|
|
97
|
-
expect(s.readyState).toBe(WebSocket.CONNECTING);
|
|
98
|
-
s.close();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
await it('default binaryType is "nodebuffer"', async () => {
|
|
102
|
-
const s = makeDeadSocket();
|
|
103
|
-
expect(s.binaryType).toBe('nodebuffer');
|
|
104
|
-
s.close();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await describe('WebSocket.send() while CONNECTING', async () => {
|
|
109
|
-
await it('throws synchronously (matches npm ws)', async () => {
|
|
110
|
-
const s = makeDeadSocket();
|
|
111
|
-
expect(() => s.send('hello')).toThrow();
|
|
112
|
-
s.close();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await it('throws synchronously even when a callback is provided', async () => {
|
|
116
|
-
// Real npm ws throws sync in both cases — the callback form is for
|
|
117
|
-
// reporting *send*-time errors once CONNECTED, not to swallow the
|
|
118
|
-
// not-open error.
|
|
119
|
-
const s = makeDeadSocket();
|
|
120
|
-
expect(() => s.send('hello', () => {})).toThrow();
|
|
121
|
-
s.close();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await on('Gjs', async () => {
|
|
126
|
-
await describe('GJS: WebSocketServer option validation', async () => {
|
|
127
|
-
await it('accepts { noServer: true } without throwing', async () => {
|
|
128
|
-
expect(() => new WebSocketServer({ noServer: true })).not.toThrow();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
await it('throws when { noServer: true } is combined with port', async () => {
|
|
132
|
-
expect(() => new WebSocketServer({ noServer: true, port: 8080 })).toThrow();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Missing port is not Phase-1-specific — real npm ws also requires it (or
|
|
138
|
-
// a `server`/`noServer` alternative). Cross-platform.
|
|
139
|
-
await describe('WebSocketServer required options', async () => {
|
|
140
|
-
await it('throws when nothing is given', async () => {
|
|
141
|
-
expect(() => new WebSocketServer({})).toThrow();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// @gjsify/ws — drop-in replacement for the `ws` npm package on Gjs.
|
|
2
|
-
//
|
|
3
|
-
// Wraps globalThis.WebSocket (provided by @gjsify/websocket over
|
|
4
|
-
// Soup.WebsocketConnection) for the CLIENT and Soup.Server.add_websocket_handler
|
|
5
|
-
// for the SERVER. Preserves the ws-npm module layout so bundlers see it as a
|
|
6
|
-
// drop-in replacement when aliased.
|
|
7
|
-
//
|
|
8
|
-
// Reference: refs/ws/index.js
|
|
9
|
-
|
|
10
|
-
import { WebSocket } from './websocket.js';
|
|
11
|
-
import { WebSocketServer } from './websocket-server.js';
|
|
12
|
-
import { createWebSocketStream } from './stream.js';
|
|
13
|
-
|
|
14
|
-
// ws index.js does these in CommonJS — we replicate on the class so
|
|
15
|
-
// `new (require('ws'))(url)` and `const { WebSocket } = require('ws')` both
|
|
16
|
-
// work. esbuild's __toESM shim turns our ESM default into an object with
|
|
17
|
-
// these properties; aliasing + the gjs CJS-compat layer handles the rest.
|
|
18
|
-
(WebSocket as any).WebSocket = WebSocket;
|
|
19
|
-
(WebSocket as any).WebSocketServer = WebSocketServer;
|
|
20
|
-
(WebSocket as any).Server = WebSocketServer;
|
|
21
|
-
(WebSocket as any).createWebSocketStream = createWebSocketStream;
|
|
22
|
-
|
|
23
|
-
export { WebSocket, WebSocketServer, createWebSocketStream };
|
|
24
|
-
export { WebSocketServer as Server };
|
|
25
|
-
export default WebSocket;
|
package/src/stream.spec.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Ported from refs/ws/test/websocket-stream.test.js
|
|
3
|
-
// Original: Copyright (c) 2011+ Einar Otto Stangvik. MIT.
|
|
4
|
-
// Rewritten for @gjsify/unit.
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, on } from '@gjsify/unit';
|
|
7
|
-
import { createServer } from 'node:http';
|
|
8
|
-
import { WebSocket, WebSocketServer, createWebSocketStream } from 'ws';
|
|
9
|
-
|
|
10
|
-
export default async () => {
|
|
11
|
-
await describe('createWebSocketStream', async () => {
|
|
12
|
-
await it('is a function', async () => {
|
|
13
|
-
expect(typeof createWebSocketStream).toBe('function');
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
await on('Gjs', async () => {
|
|
18
|
-
await describe('createWebSocketStream — server side', async () => {
|
|
19
|
-
await it('pipes data from client through server duplex and back', async () => {
|
|
20
|
-
const server = createServer();
|
|
21
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
22
|
-
|
|
23
|
-
server.on('upgrade', (req: any, socket: any, head: any) => {
|
|
24
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => wss.emit('connection', ws, req));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await new Promise<void>((resolve, reject) => {
|
|
28
|
-
wss.on('connection', (ws: any) => {
|
|
29
|
-
const stream = createWebSocketStream(ws);
|
|
30
|
-
stream.pipe(stream); // echo: readable piped back into writable
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
server.listen(0, () => {
|
|
34
|
-
const addr = server.address() as any;
|
|
35
|
-
const client = new WebSocket(`ws://127.0.0.1:${addr.port}/`);
|
|
36
|
-
|
|
37
|
-
client.on('open', () => client.send('hello stream'));
|
|
38
|
-
|
|
39
|
-
client.on('message', (data: any) => {
|
|
40
|
-
expect(String(data)).toBe('hello stream');
|
|
41
|
-
client.close();
|
|
42
|
-
server.close();
|
|
43
|
-
wss.close();
|
|
44
|
-
resolve();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
client.on('error', reject);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await it('push(null) on WebSocket close ends the readable side', async () => {
|
|
53
|
-
const server = createServer();
|
|
54
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
55
|
-
|
|
56
|
-
server.on('upgrade', (req: any, socket: any, head: any) => {
|
|
57
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => wss.emit('connection', ws, req));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await new Promise<void>((resolve, reject) => {
|
|
61
|
-
wss.on('connection', (ws: any) => {
|
|
62
|
-
const stream = createWebSocketStream(ws);
|
|
63
|
-
const chunks: Buffer[] = [];
|
|
64
|
-
|
|
65
|
-
stream.on('data', (chunk: Buffer) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
66
|
-
stream.on('end', () => {
|
|
67
|
-
expect(Buffer.concat(chunks).toString()).toBe('end-test');
|
|
68
|
-
server.close();
|
|
69
|
-
wss.close();
|
|
70
|
-
resolve();
|
|
71
|
-
});
|
|
72
|
-
stream.on('error', reject);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
server.listen(0, () => {
|
|
76
|
-
const addr = server.address() as any;
|
|
77
|
-
const client = new WebSocket(`ws://127.0.0.1:${addr.port}/`);
|
|
78
|
-
|
|
79
|
-
client.on('open', () => {
|
|
80
|
-
client.send('end-test');
|
|
81
|
-
client.close();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
client.on('error', reject);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
await it('writes to the duplex are sent as WebSocket messages', async () => {
|
|
90
|
-
const server = createServer();
|
|
91
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
92
|
-
|
|
93
|
-
server.on('upgrade', (req: any, socket: any, head: any) => {
|
|
94
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => wss.emit('connection', ws, req));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
await new Promise<void>((resolve, reject) => {
|
|
98
|
-
wss.on('connection', (ws: any) => {
|
|
99
|
-
const stream = createWebSocketStream(ws);
|
|
100
|
-
stream.write('from server via stream');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
server.listen(0, () => {
|
|
104
|
-
const addr = server.address() as any;
|
|
105
|
-
const client = new WebSocket(`ws://127.0.0.1:${addr.port}/`);
|
|
106
|
-
|
|
107
|
-
client.on('message', (data: any) => {
|
|
108
|
-
expect(String(data)).toBe('from server via stream');
|
|
109
|
-
client.close();
|
|
110
|
-
server.close();
|
|
111
|
-
wss.close();
|
|
112
|
-
resolve();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
client.on('error', reject);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
};
|
package/src/stream.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Reference: refs/ws/lib/stream.js
|
|
2
|
-
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>. MIT.
|
|
3
|
-
// Adapted for @gjsify/ws — WebSocket → Node.js Duplex bridge without _socket dependency.
|
|
4
|
-
|
|
5
|
-
import { Duplex } from 'node:stream';
|
|
6
|
-
|
|
7
|
-
function emitClose(stream: Duplex): void {
|
|
8
|
-
stream.emit('close');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function duplexOnEnd(this: Duplex): void {
|
|
12
|
-
if (!this.destroyed && (this as any)._writableState.finished) {
|
|
13
|
-
this.destroy();
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function duplexOnError(this: Duplex, err: Error): void {
|
|
18
|
-
this.removeListener('error', duplexOnError);
|
|
19
|
-
this.destroy();
|
|
20
|
-
if (this.listenerCount('error') === 0) {
|
|
21
|
-
this.emit('error', err);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function createWebSocketStream(ws: any, options: Record<string, unknown> = {}): Duplex {
|
|
26
|
-
let terminateOnDestroy = true;
|
|
27
|
-
|
|
28
|
-
const duplex = new Duplex({
|
|
29
|
-
...options,
|
|
30
|
-
autoDestroy: false,
|
|
31
|
-
emitClose: false,
|
|
32
|
-
objectMode: false,
|
|
33
|
-
writableObjectMode: false,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
ws.on('message', (msg: Buffer | string, isBinary: boolean) => {
|
|
37
|
-
let data: Buffer | string;
|
|
38
|
-
if (isBinary || duplex.readableObjectMode) {
|
|
39
|
-
data = msg;
|
|
40
|
-
} else {
|
|
41
|
-
data = typeof msg === 'string' ? Buffer.from(msg) : msg;
|
|
42
|
-
}
|
|
43
|
-
if (!duplex.push(data) && typeof ws.pause === 'function') ws.pause();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
ws.once('error', (err: Error) => {
|
|
47
|
-
if (duplex.destroyed) return;
|
|
48
|
-
terminateOnDestroy = false;
|
|
49
|
-
duplex.destroy(err);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
ws.once('close', () => {
|
|
53
|
-
if (duplex.destroyed) return;
|
|
54
|
-
duplex.push(null);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
(duplex as any)._destroy = function (err: Error | null, callback: (err: Error | null) => void): void {
|
|
58
|
-
if (ws.readyState === ws.CLOSED) {
|
|
59
|
-
callback(err);
|
|
60
|
-
process.nextTick(emitClose, duplex);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let called = false;
|
|
65
|
-
|
|
66
|
-
ws.once('error', (e: Error) => {
|
|
67
|
-
called = true;
|
|
68
|
-
callback(e);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
ws.once('close', () => {
|
|
72
|
-
if (!called) callback(err);
|
|
73
|
-
process.nextTick(emitClose, duplex);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (terminateOnDestroy) ws.terminate();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
(duplex as any)._final = function (callback: () => void): void {
|
|
80
|
-
if (ws.readyState === ws.CONNECTING) {
|
|
81
|
-
ws.once('open', () => (duplex as any)._final(callback));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
|
85
|
-
callback();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
ws.once('close', callback);
|
|
89
|
-
ws.close();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
duplex._read = function (): void {
|
|
93
|
-
if (typeof ws.resume === 'function') ws.resume();
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
duplex._write = function (chunk: Buffer | string, _encoding: BufferEncoding, callback: (err?: Error) => void): void {
|
|
97
|
-
if (ws.readyState === ws.CONNECTING) {
|
|
98
|
-
ws.once('open', () => duplex._write(chunk, _encoding, callback));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
ws.send(chunk, callback);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
duplex.on('end', duplexOnEnd);
|
|
105
|
-
duplex.on('error', duplexOnError);
|
|
106
|
-
return duplex;
|
|
107
|
-
}
|