@gjsify/websocket 0.51.1 → 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/globals.mjs +6 -0
- package/lib/esm/abort.js +1 -0
- package/lib/esm/index.js +1 -1
- package/lib/types/abort.d.ts +46 -0
- package/lib/types/index.d.ts +21 -1
- package/package.json +8 -8
package/globals.mjs
CHANGED
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
export const WebSocket = globalThis.WebSocket;
|
|
6
6
|
export const MessageEvent = globalThis.MessageEvent;
|
|
7
7
|
export const CloseEvent = globalThis.CloseEvent;
|
|
8
|
+
|
|
9
|
+
// The internal hook @gjsify/ws reads (src/abort.ts). Symbol.for() yields the
|
|
10
|
+
// same key; a host WebSocket carries no method under it, so ws falls back to
|
|
11
|
+
// the W3C calls. No Soup here, so no Soup transport error either.
|
|
12
|
+
export const kClose = Symbol.for('gjsify.websocket.close');
|
|
13
|
+
export const isTransportFailure = () => false;
|
package/lib/esm/abort.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import t from"@girs/gio-2.0";import n from"@girs/soup-3.0";const r=Symbol.for(`gjsify.websocket.close`),i=1006;function soupCloseCode(e,t){let r=e.get_connection_type()===n.WebsocketConnectionType.SERVER;return t===1010?r?1002:t:t===1011?r?t:1002:t>=1e3&&t<=1003||t>=1007&&t<=1009||t>=3e3&&t<=4999?t:1002}function isRefusedEcho(e,t){let r=e.get_close_code();return t.matches(n.websocket_error_quark(),n.WebsocketCloseCode.PROTOCOL_ERROR)&&r>=1012&&r<=1014}function isTransportFailure(n){return n instanceof e.Error&&(n.domain===t.io_error_quark()||n.domain===t.tls_error_quark())}function socketOf(e){let n=e;for(let e=0;n&&e<4;e++){if(n instanceof t.SocketConnection)return n.get_socket();let e=n;n=e.base_iostream??e.base_io_stream}return null}function abortConnection(e){let t=socketOf(e.get_io_stream());if(t)try{t.shutdown(!0,!0)}catch{}else e.get_state()===n.WebsocketState.OPEN&&e.close(n.WebsocketCloseCode.GOING_AWAY,null)}export{i as ABORT_CODE,abortConnection,isRefusedEcho,isTransportFailure,r as kClose,soupCloseCode};
|
package/lib/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import e from"@girs/glib-2.0";import
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{ABORT_CODE as e,abortConnection as t,isRefusedEcho as n,isTransportFailure as r,kClose as i,soupCloseCode as a}from"./abort.js";import o from"@girs/glib-2.0";import s from"@girs/gio-2.0";import c from"@girs/soup-3.0";import{CloseEvent as l,Event as u,EventTarget as d,MessageEvent as f}from"@gjsify/dom-events";function extensionName(e){return e instanceof c.WebsocketExtensionDeflate?`permessage-deflate`:(e.constructor.$gtype?.name??``).replace(/^SoupWebsocketExtension/,``).toLowerCase()}function serializeExtensions(e){return!e||e.length===0?``:e.map(e=>{let t=e.get_response_params();return t?`${extensionName(e)}${t}`:extensionName(e)}).join(`, `)}function acceptsAnyCertificate(e){return e===void 0?o.getenv(`NODE_TLS_REJECT_UNAUTHORIZED`)===`0`:!e}var WebSocket=class extends d{static CONNECTING=0;static OPEN=1;static CLOSING=2;static CLOSED=3;CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;url;readyState=0;bufferedAmount=0;extensions=``;protocol=``;binaryType=`blob`;onopen=null;onmessage=null;onerror=null;onclose=null;_connection=null;_session;_protocols;_cancellable=new s.Cancellable;_failReason=null;_handshakeTimer=null;constructor(e,t,n){super(),this.url=typeof e==`string`?e:e.toString(),this._protocols=typeof t==`string`?[t]:t??[],this._session=new c.Session,n?.perMessageDeflate&&(this._session.add_feature_by_type(c.WebsocketExtensionManager.$gtype),this._session.add_feature_by_type(c.WebsocketExtensionDeflate.$gtype)),this._connect(n)}_connect(e){let r=o.Uri.parse(this.url,o.UriFlags.NONE);r.get_path()||(r=o.Uri.build(o.UriFlags.NONE,r.get_scheme(),r.get_userinfo(),r.get_host(),r.get_port(),`/`,r.get_query(),r.get_fragment()));let i=new c.Message({method:`GET`,uri:r});if(e?.headers){let t=i.get_request_headers();for(let[n,r]of Object.entries(e.headers))Array.isArray(r)?r.forEach(e=>t.append(n,e)):t.replace(n,r)}r.get_scheme()===`wss`&&acceptsAnyCertificate(e?.rejectUnauthorized)&&i.connect(`accept-certificate`,()=>!0),e?.handshakeTimeout&&(this._handshakeTimer=setTimeout(()=>{this._handshakeTimer=null,this._failConnecting(`Opening handshake has timed out`)},e.handshakeTimeout)),this._session.websocket_connect_async(i,e?.origin??null,this._protocols.length>0?this._protocols:null,o.PRIORITY_DEFAULT,this._cancellable,(e,r)=>{this._handshakeTimer!==null&&(clearTimeout(this._handshakeTimer),this._handshakeTimer=null);try{let e=this._session.websocket_connect_finish(r);if(this._failReason!==null)throw t(e),Error(this._failReason);this._connection=e,this._connection.max_incoming_payload_size=100*1024*1024,this.readyState=1,this.protocol=this._connection.get_protocol()??``,this.extensions=serializeExtensions(this._connection.get_extensions()),this._connection.connect(`message`,(e,t,n)=>{this._onMessage(t,n)}),this._connection.connect(`closing`,()=>{this.readyState===1&&(this.readyState=2)}),this._connection.connect(`closed`,()=>{this._onClosed()}),this._connection.connect(`error`,(e,t)=>{n(e,t)||this._onError(t)});let i=new u(`open`);this.dispatchEvent(i),this.onopen&&this.onopen.call(this,i)}catch(e){this.readyState=3;let t=this._failReason??(e instanceof Error?e.message:String(e)),n=Error(t),r=new u(`error`);r.error=n,r.message=t,this.dispatchEvent(r),this.onerror&&this.onerror.call(this,r);let i=new l(`close`,{code:1006,reason:``,wasClean:!1});this.dispatchEvent(i),this.onclose&&this.onclose.call(this,i)}})}_onMessage(e,t){let n;if(e===c.WebsocketDataType.TEXT)n=new TextDecoder().decode(t.toArray());else{let e=t.toArray();n=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)}let r=new f(`message`,{data:n,origin:this.url});this.dispatchEvent(r),this.onmessage&&this.onmessage.call(this,r)}_onError(e){let t=new u(`error`);t.error=Error(e.message,{cause:e}),t.message=e.message,this.dispatchEvent(t),this.onerror&&this.onerror.call(this,t)}_onClosed(){let e=this._connection?.get_close_code()||1006,t=this._connection?.get_close_data()??``,n=e!==1006;this.readyState=3,this._connection=null;let r=new l(`close`,{code:e,reason:t,wasClean:n});this.dispatchEvent(r),this.onclose&&this.onclose.call(this,r)}send(e){if(this.readyState===0)throw new DOMException(`WebSocket is not open`,`InvalidStateError`);if(this.readyState!==1||!this._isSoupOpen()){this.bufferedAmount+=typeof e==`string`?new TextEncoder().encode(e).byteLength:e.byteLength;return}if(this._connection)if(typeof e==`string`){let t=new TextEncoder().encode(e);this._connection.send_message(c.WebsocketDataType.TEXT,new o.Bytes(t))}else{let t;t=e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength),this._connection.send_message(c.WebsocketDataType.BINARY,new o.Bytes(t))}}close(e,t){if(e!==void 0&&e!==1e3&&(e<3e3||e>4999))throw new DOMException(`The code must be either 1000, or between 3000 and 4999. ${e} is neither.`,`InvalidAccessError`);if(t!==void 0&&new TextEncoder().encode(t).byteLength>123)throw new DOMException(`The close reason must not be greater than 123 UTF-8 bytes.`,`SyntaxError`);this[i](e,t)}[i](n,r){if(this.readyState===3)return;let i=n===e;if(!(this.readyState===2&&!i)){if(!this._connection){this._failConnecting(`WebSocket was closed before the connection was established`);return}if(this.readyState=2,i){t(this._connection);return}this._isSoupOpen()&&this._connection.close(a(this._connection,n??1e3),r??null)}}_failConnecting(e){this.readyState===3||this._failReason!==null||(this._failReason=e,this.readyState=2,this._handshakeTimer!==null&&(clearTimeout(this._handshakeTimer),this._handshakeTimer=null),this._cancellable.cancel())}_isSoupOpen(){return this._connection?.get_state()===c.WebsocketState.OPEN}};export{l as CloseEvent,f as MessageEvent,WebSocket,WebSocket as default,t as abortConnection,n as isRefusedEcho,r as isTransportFailure,i as kClose,a as soupCloseCode};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import GLib from '@girs/glib-2.0';
|
|
2
|
+
import Soup from '@girs/soup-3.0';
|
|
3
|
+
/** The one internal hook on {@link WebSocket} instances: close() without the
|
|
4
|
+
* W3C restriction on `code`. The spec's close() admits only 1000 and
|
|
5
|
+
* 3000–4999 from script; npm ws also sends 1001–1014, and its terminate()
|
|
6
|
+
* drops the connection with no Close frame at all. `@gjsify/ws` validates by
|
|
7
|
+
* ws's rules and calls this for both. 1006 is RFC 6455's code for "closed
|
|
8
|
+
* without a Close frame" and never goes on the wire, so it selects the
|
|
9
|
+
* abort: `[kClose](1006)` is terminate(), any other code a Close frame.
|
|
10
|
+
* @internal */
|
|
11
|
+
export declare const kClose: unique symbol;
|
|
12
|
+
/** The status code that makes {@link kClose} abort instead of closing. */
|
|
13
|
+
export declare const ABORT_CODE = 1006;
|
|
14
|
+
/** The code Soup will actually put in a Close frame for `code`. libsoup's
|
|
15
|
+
* close_connection() knows only 1000–1003, 1007–1011 and 3000–4999 —
|
|
16
|
+
* RFC 6455's original registry — and restricts 1010 to clients and 1011 to
|
|
17
|
+
* servers. An unknown code (1012–1014, registered later) it turns into a
|
|
18
|
+
* protocol error plus an 'error' signal; a role-restricted one is a
|
|
19
|
+
* g_return_if_fail CRITICAL that sends nothing and leaves the connection
|
|
20
|
+
* open. Both are worse than the substitute libsoup itself falls back to, so
|
|
21
|
+
* we send 1002 directly: the connection closes, no CRITICAL, no spurious
|
|
22
|
+
* 'error'. Upstream: status/upstream-patch-candidates.md.
|
|
23
|
+
* @internal */
|
|
24
|
+
export declare function soupCloseCode(conn: Soup.WebsocketConnection, code: number): number;
|
|
25
|
+
/** Whether an error Soup emitted is its own refusal to echo a peer's valid
|
|
26
|
+
* close code. On a received Close, libsoup answers through the same
|
|
27
|
+
* close_connection() that rejects 1012–1014 (see soupCloseCode), so a peer's
|
|
28
|
+
* "service restart" raises a protocol error although the peer broke nothing.
|
|
29
|
+
* The close still carries the peer's code; only the 'error' is spurious — for
|
|
30
|
+
* the W3C API and npm ws alike.
|
|
31
|
+
* @internal */
|
|
32
|
+
export declare function isRefusedEcho(conn: Soup.WebsocketConnection, error: GLib.Error): boolean;
|
|
33
|
+
/** Whether an error Soup's WebsocketConnection emitted is the transport
|
|
34
|
+
* failing — a reset, a TLS EOF — rather than the peer breaking the protocol
|
|
35
|
+
* (Soup.WebsocketError). npm ws reports only the latter as 'error'; the
|
|
36
|
+
* former ends in 'close' (1006) alone, which Soup emits next.
|
|
37
|
+
* @internal */
|
|
38
|
+
export declare function isTransportFailure(error: unknown): boolean;
|
|
39
|
+
/** Drop the connection without a Close frame. Soup's close() cannot: it
|
|
40
|
+
* rejects 1006 with a CRITICAL and sends nothing. Shutting the socket down —
|
|
41
|
+
* never closing it — lets Soup's pending read see EOF, record a dirty close
|
|
42
|
+
* and emit 'closed' itself, exactly once; Soup still owns and closes the
|
|
43
|
+
* stream. Closing the fd instead would leave Soup polling a dead descriptor
|
|
44
|
+
* and emitting 'error'.
|
|
45
|
+
* @internal */
|
|
46
|
+
export declare function abortConnection(conn: Soup.WebsocketConnection): void;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Event, EventTarget, MessageEvent, CloseEvent } from '@gjsify/dom-events';
|
|
2
|
+
import { abortConnection, isRefusedEcho, isTransportFailure, kClose, soupCloseCode } from './abort.js';
|
|
2
3
|
export { MessageEvent, CloseEvent };
|
|
4
|
+
export { abortConnection, isRefusedEcho, isTransportFailure, kClose, soupCloseCode };
|
|
3
5
|
/** Options for the WebSocket constructor. Extends the W3C spec with Soup-level knobs. */
|
|
4
6
|
export interface WebSocketOptions {
|
|
5
7
|
/** Offer permessage-deflate (RFC 7692) during the WebSocket handshake.
|
|
@@ -21,6 +23,11 @@ export interface WebSocketOptions {
|
|
|
21
23
|
/** Abort the opening handshake after this many milliseconds. Fires an
|
|
22
24
|
* 'error' event with message "Opening handshake has timed out". */
|
|
23
25
|
handshakeTimeout?: number;
|
|
26
|
+
/** `false` accepts any server certificate on a `wss:` URL — npm ws's
|
|
27
|
+
* (and Node TLS's) option of the same name. Applies to this connection's
|
|
28
|
+
* upgrade message only. Unset, `NODE_TLS_REJECT_UNAUTHORIZED=0` opts
|
|
29
|
+
* out as it does in Node; `true` keeps verification regardless. */
|
|
30
|
+
rejectUnauthorized?: boolean;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
33
|
* W3C WebSocket API implementation using Soup 3.0.
|
|
@@ -54,7 +61,8 @@ export declare class WebSocket extends EventTarget {
|
|
|
54
61
|
private _session;
|
|
55
62
|
private _protocols;
|
|
56
63
|
private _cancellable;
|
|
57
|
-
|
|
64
|
+
/** Why the handshake is being abandoned; the failure path reports it. */
|
|
65
|
+
private _failReason;
|
|
58
66
|
private _handshakeTimer;
|
|
59
67
|
constructor(url: string | URL, protocols?: string | string[], options?: WebSocketOptions);
|
|
60
68
|
private _connect;
|
|
@@ -78,5 +86,17 @@ export declare class WebSocket extends EventTarget {
|
|
|
78
86
|
* Close the WebSocket connection.
|
|
79
87
|
*/
|
|
80
88
|
close(code?: number, reason?: string): void;
|
|
89
|
+
/** @internal close() without the W3C restriction on `code`: any status
|
|
90
|
+
* RFC 6455 lets an endpoint send (1000–1003, 1007–1014, 3000–4999),
|
|
91
|
+
* which is what npm ws accepts, or {@link ABORT_CODE} to drop the
|
|
92
|
+
* connection without a Close frame (ws's terminate()); 'close' then
|
|
93
|
+
* follows with 1006. The caller validates. Codes libsoup cannot send go
|
|
94
|
+
* out as 1002 (see soupCloseCode). See {@link kClose}. */
|
|
95
|
+
[kClose](code?: number, reason?: string): void;
|
|
96
|
+
/** Abandon the handshake. The connect callback runs with the cancel
|
|
97
|
+
* error and reports `reason` — asynchronously, as the spec's "fail the
|
|
98
|
+
* WebSocket connection" queues its events. The first reason wins. */
|
|
99
|
+
private _failConnecting;
|
|
100
|
+
private _isSoupOpen;
|
|
81
101
|
}
|
|
82
102
|
export default WebSocket;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/websocket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "W3C WebSocket API for GJS using Soup 3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
"soup"
|
|
43
43
|
],
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@gjsify/cli": "^0.
|
|
46
|
-
"@gjsify/unit": "^0.
|
|
45
|
+
"@gjsify/cli": "^0.53.0",
|
|
46
|
+
"@gjsify/unit": "^0.53.0",
|
|
47
47
|
"@types/node": "^25.9.2",
|
|
48
48
|
"typescript": "^6.0.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@girs/gio-2.0": "5.
|
|
52
|
-
"@girs/gjs": "5.
|
|
53
|
-
"@girs/glib-2.0": "5.
|
|
54
|
-
"@girs/soup-3.0": "5.
|
|
55
|
-
"@gjsify/dom-events": "^0.
|
|
51
|
+
"@girs/gio-2.0": "5.4.0",
|
|
52
|
+
"@girs/gjs": "5.4.0",
|
|
53
|
+
"@girs/glib-2.0": "5.4.0",
|
|
54
|
+
"@girs/soup-3.0": "5.4.0",
|
|
55
|
+
"@gjsify/dom-events": "^0.53.0"
|
|
56
56
|
},
|
|
57
57
|
"gjsify": {
|
|
58
58
|
"runtimes": {
|