@gjsify/websocket 0.3.13 → 0.3.15

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/index.js CHANGED
@@ -1,216 +1,224 @@
1
1
  import GLib from "@girs/glib-2.0";
2
2
  import Soup from "@girs/soup-3.0";
3
3
  import Gio from "@girs/gio-2.0";
4
- import { Event, EventTarget, MessageEvent, CloseEvent } from "@gjsify/dom-events";
4
+ import { CloseEvent, Event, EventTarget, MessageEvent } from "@gjsify/dom-events";
5
+
6
+ //#region src/index.ts
5
7
  const CONNECTING = 0;
6
8
  const OPEN = 1;
7
9
  const CLOSING = 2;
8
10
  const CLOSED = 3;
9
11
  function extensionName(ext) {
10
- if (ext instanceof Soup.WebsocketExtensionDeflate) return "permessage-deflate";
11
- const gtype = ext.constructor.$gtype?.name ?? "";
12
- return gtype.replace(/^SoupWebsocketExtension/, "").toLowerCase();
12
+ if (ext instanceof Soup.WebsocketExtensionDeflate) return "permessage-deflate";
13
+ const gtype = ext.constructor.$gtype?.name ?? "";
14
+ return gtype.replace(/^SoupWebsocketExtension/, "").toLowerCase();
13
15
  }
14
16
  function serializeExtensions(exts) {
15
- if (!exts || exts.length === 0) return "";
16
- return exts.map((ext) => {
17
- const params = ext.get_response_params();
18
- return params ? `${extensionName(ext)}${params}` : extensionName(ext);
19
- }).join(", ");
17
+ if (!exts || exts.length === 0) return "";
18
+ return exts.map((ext) => {
19
+ const params = ext.get_response_params();
20
+ return params ? `${extensionName(ext)}${params}` : extensionName(ext);
21
+ }).join(", ");
20
22
  }
21
- class WebSocket extends EventTarget {
22
- // readyState constants
23
- static CONNECTING = CONNECTING;
24
- static OPEN = OPEN;
25
- static CLOSING = CLOSING;
26
- static CLOSED = CLOSED;
27
- CONNECTING = CONNECTING;
28
- OPEN = OPEN;
29
- CLOSING = CLOSING;
30
- CLOSED = CLOSED;
31
- url;
32
- readyState = CONNECTING;
33
- bufferedAmount = 0;
34
- extensions = "";
35
- protocol = "";
36
- binaryType = "blob";
37
- // Event handlers (attribute-style)
38
- onopen = null;
39
- onmessage = null;
40
- onerror = null;
41
- onclose = null;
42
- _connection = null;
43
- _session;
44
- _protocols;
45
- _cancellable = null;
46
- _handshakeTimedOut = false;
47
- _handshakeTimer = null;
48
- constructor(url, protocols, options) {
49
- super();
50
- this.url = typeof url === "string" ? url : url.toString();
51
- this._protocols = typeof protocols === "string" ? [protocols] : protocols ?? [];
52
- this._session = new Soup.Session();
53
- if (options?.perMessageDeflate) {
54
- this._session.add_feature_by_type(Soup.WebsocketExtensionManager.$gtype);
55
- this._session.add_feature_by_type(Soup.WebsocketExtensionDeflate.$gtype);
56
- }
57
- this._connect(options);
58
- }
59
- _connect(options) {
60
- const uri = GLib.Uri.parse(this.url, GLib.UriFlags.NONE);
61
- const msg = new Soup.Message({ method: "GET", uri });
62
- if (options?.headers) {
63
- const reqHeaders = msg.get_request_headers();
64
- for (const [name, value] of Object.entries(options.headers)) {
65
- if (Array.isArray(value)) {
66
- value.forEach((v) => reqHeaders.append(name, v));
67
- } else {
68
- reqHeaders.replace(name, value);
69
- }
70
- }
71
- }
72
- if (options?.handshakeTimeout) {
73
- this._cancellable = new Gio.Cancellable();
74
- const cancellable = this._cancellable;
75
- this._handshakeTimer = setTimeout(() => {
76
- this._handshakeTimedOut = true;
77
- this._handshakeTimer = null;
78
- cancellable.cancel();
79
- }, options.handshakeTimeout);
80
- }
81
- this._session.websocket_connect_async(
82
- msg,
83
- options?.origin ?? null,
84
- this._protocols.length > 0 ? this._protocols : null,
85
- GLib.PRIORITY_DEFAULT,
86
- this._cancellable,
87
- (_self, asyncRes) => {
88
- if (this._handshakeTimer !== null) {
89
- clearTimeout(this._handshakeTimer);
90
- this._handshakeTimer = null;
91
- }
92
- try {
93
- this._connection = this._session.websocket_connect_finish(asyncRes);
94
- this._connection.max_incoming_payload_size = 100 * 1024 * 1024;
95
- this.readyState = OPEN;
96
- this.protocol = this._connection.get_protocol() ?? "";
97
- this.extensions = serializeExtensions(this._connection.get_extensions());
98
- this._connection.connect("message", (_conn, type, message) => {
99
- this._onMessage(type, message);
100
- });
101
- this._connection.connect("closed", () => {
102
- this._onClosed();
103
- });
104
- this._connection.connect("error", (_conn, error) => {
105
- this._onError(error);
106
- });
107
- const openEvent = new Event("open");
108
- this.dispatchEvent(openEvent);
109
- if (this.onopen) this.onopen.call(this, openEvent);
110
- } catch (error) {
111
- this.readyState = CLOSED;
112
- const errorMessage = this._handshakeTimedOut ? "Opening handshake has timed out" : error instanceof Error ? error.message : String(error);
113
- const err = new Error(errorMessage);
114
- const errorEvent = new Event("error");
115
- errorEvent.error = err;
116
- errorEvent.message = errorMessage;
117
- this.dispatchEvent(errorEvent);
118
- if (this.onerror) this.onerror.call(this, errorEvent);
119
- const closeEvent = new CloseEvent("close", {
120
- code: 1006,
121
- reason: errorMessage,
122
- wasClean: false
123
- });
124
- this.dispatchEvent(closeEvent);
125
- if (this.onclose) this.onclose.call(this, closeEvent);
126
- }
127
- }
128
- );
129
- }
130
- _onMessage(type, message) {
131
- let data;
132
- if (type === Soup.WebsocketDataType.TEXT) {
133
- const decoder = new TextDecoder();
134
- data = decoder.decode(message.toArray());
135
- } else {
136
- const arr = message.toArray();
137
- data = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
138
- }
139
- const event = new MessageEvent("message", { data, origin: this.url });
140
- this.dispatchEvent(event);
141
- if (this.onmessage) this.onmessage.call(this, event);
142
- }
143
- _onError(error) {
144
- const event = new Event("error");
145
- this.dispatchEvent(event);
146
- if (this.onerror) this.onerror.call(this, event);
147
- }
148
- _onClosed() {
149
- const code = this._connection?.get_close_code() ?? 1006;
150
- const reason = this._connection?.get_close_data() ?? "";
151
- const wasClean = code === 1e3;
152
- this.readyState = CLOSED;
153
- this._connection = null;
154
- const event = new CloseEvent("close", { code, reason, wasClean });
155
- this.dispatchEvent(event);
156
- if (this.onclose) this.onclose.call(this, event);
157
- }
158
- /**
159
- * Send data through the WebSocket connection.
160
- *
161
- * For strings, we intentionally route through `send_message(TEXT, bytes)`
162
- * rather than the simpler `send_text(str)` API. Reason: `send_text()`
163
- * takes a C `const char*` (null-terminated), so any embedded `\x00` in
164
- * the JS string gets truncated at the first NUL at the GI boundary —
165
- * Autobahn case 6.7.1 (single NUL in a text frame) was returned as an
166
- * empty string. Going through `GLib.Bytes` preserves the exact byte
167
- * sequence; Soup still sets the text-frame opcode because we pass
168
- * `Soup.WebsocketDataType.TEXT` explicitly.
169
- */
170
- send(data) {
171
- if (this.readyState !== OPEN) {
172
- throw new DOMException("WebSocket is not open", "InvalidStateError");
173
- }
174
- if (!this._connection) return;
175
- if (typeof data === "string") {
176
- const bytes = new TextEncoder().encode(data);
177
- this._connection.send_message(Soup.WebsocketDataType.TEXT, new GLib.Bytes(bytes));
178
- } else {
179
- let bytes;
180
- if (data instanceof ArrayBuffer) {
181
- bytes = new Uint8Array(data);
182
- } else {
183
- bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
184
- }
185
- this._connection.send_message(Soup.WebsocketDataType.BINARY, new GLib.Bytes(bytes));
186
- }
187
- }
188
- /**
189
- * Close the WebSocket connection.
190
- */
191
- close(code, reason) {
192
- if (this.readyState === CLOSED || this.readyState === CLOSING) return;
193
- if (code !== void 0 && code !== 1e3 && (code < 3e3 || code > 4999)) {
194
- throw new DOMException(
195
- `The code must be either 1000, or between 3000 and 4999. ${code} is neither.`,
196
- "InvalidAccessError"
197
- );
198
- }
199
- this.readyState = CLOSING;
200
- if (this._connection) {
201
- this._connection.close(code ?? 1e3, reason ?? null);
202
- } else {
203
- this.readyState = CLOSED;
204
- const event = new CloseEvent("close", { code: 1006, wasClean: false });
205
- this.dispatchEvent(event);
206
- if (this.onclose) this.onclose.call(this, event);
207
- }
208
- }
209
- }
210
- var index_default = WebSocket;
211
- export {
212
- CloseEvent,
213
- MessageEvent,
214
- WebSocket,
215
- index_default as default
23
+ /**
24
+ * W3C WebSocket API implementation using Soup 3.0.
25
+ *
26
+ * Usage:
27
+ * const ws = new WebSocket('ws://localhost:8080/');
28
+ * ws.onopen = () => ws.send('hello');
29
+ * ws.onmessage = (e) => console.log(e.data);
30
+ * ws.onclose = (e) => console.log('closed', e.code);
31
+ */
32
+ var WebSocket = class extends EventTarget {
33
+ static CONNECTING = CONNECTING;
34
+ static OPEN = OPEN;
35
+ static CLOSING = CLOSING;
36
+ static CLOSED = CLOSED;
37
+ CONNECTING = CONNECTING;
38
+ OPEN = OPEN;
39
+ CLOSING = CLOSING;
40
+ CLOSED = CLOSED;
41
+ url;
42
+ readyState = CONNECTING;
43
+ bufferedAmount = 0;
44
+ extensions = "";
45
+ protocol = "";
46
+ binaryType = "blob";
47
+ onopen = null;
48
+ onmessage = null;
49
+ onerror = null;
50
+ onclose = null;
51
+ _connection = null;
52
+ _session;
53
+ _protocols;
54
+ _cancellable = null;
55
+ _handshakeTimedOut = false;
56
+ _handshakeTimer = null;
57
+ constructor(url, protocols, options) {
58
+ super();
59
+ this.url = typeof url === "string" ? url : url.toString();
60
+ this._protocols = typeof protocols === "string" ? [protocols] : protocols ?? [];
61
+ this._session = new Soup.Session();
62
+ if (options?.perMessageDeflate) {
63
+ this._session.add_feature_by_type(Soup.WebsocketExtensionManager.$gtype);
64
+ this._session.add_feature_by_type(Soup.WebsocketExtensionDeflate.$gtype);
65
+ }
66
+ this._connect(options);
67
+ }
68
+ _connect(options) {
69
+ const uri = GLib.Uri.parse(this.url, GLib.UriFlags.NONE);
70
+ const msg = new Soup.Message({
71
+ method: "GET",
72
+ uri
73
+ });
74
+ if (options?.headers) {
75
+ const reqHeaders = msg.get_request_headers();
76
+ for (const [name, value] of Object.entries(options.headers)) {
77
+ if (Array.isArray(value)) {
78
+ value.forEach((v) => reqHeaders.append(name, v));
79
+ } else {
80
+ reqHeaders.replace(name, value);
81
+ }
82
+ }
83
+ }
84
+ if (options?.handshakeTimeout) {
85
+ this._cancellable = new Gio.Cancellable();
86
+ const cancellable = this._cancellable;
87
+ this._handshakeTimer = setTimeout(() => {
88
+ this._handshakeTimedOut = true;
89
+ this._handshakeTimer = null;
90
+ cancellable.cancel();
91
+ }, options.handshakeTimeout);
92
+ }
93
+ this._session.websocket_connect_async(msg, options?.origin ?? null, this._protocols.length > 0 ? this._protocols : null, GLib.PRIORITY_DEFAULT, this._cancellable, (_self, asyncRes) => {
94
+ if (this._handshakeTimer !== null) {
95
+ clearTimeout(this._handshakeTimer);
96
+ this._handshakeTimer = null;
97
+ }
98
+ try {
99
+ this._connection = this._session.websocket_connect_finish(asyncRes);
100
+ this._connection.max_incoming_payload_size = 100 * 1024 * 1024;
101
+ this.readyState = OPEN;
102
+ this.protocol = this._connection.get_protocol() ?? "";
103
+ this.extensions = serializeExtensions(this._connection.get_extensions());
104
+ this._connection.connect("message", (_conn, type, message) => {
105
+ this._onMessage(type, message);
106
+ });
107
+ this._connection.connect("closed", () => {
108
+ this._onClosed();
109
+ });
110
+ this._connection.connect("error", (_conn, error) => {
111
+ this._onError(error);
112
+ });
113
+ const openEvent = new Event("open");
114
+ this.dispatchEvent(openEvent);
115
+ if (this.onopen) this.onopen.call(this, openEvent);
116
+ } catch (error) {
117
+ this.readyState = CLOSED;
118
+ const errorMessage = this._handshakeTimedOut ? "Opening handshake has timed out" : error instanceof Error ? error.message : String(error);
119
+ const err = new Error(errorMessage);
120
+ const errorEvent = new Event("error");
121
+ errorEvent.error = err;
122
+ errorEvent.message = errorMessage;
123
+ this.dispatchEvent(errorEvent);
124
+ if (this.onerror) this.onerror.call(this, errorEvent);
125
+ const closeEvent = new CloseEvent("close", {
126
+ code: 1006,
127
+ reason: errorMessage,
128
+ wasClean: false
129
+ });
130
+ this.dispatchEvent(closeEvent);
131
+ if (this.onclose) this.onclose.call(this, closeEvent);
132
+ }
133
+ });
134
+ }
135
+ _onMessage(type, message) {
136
+ let data;
137
+ if (type === Soup.WebsocketDataType.TEXT) {
138
+ const decoder = new TextDecoder();
139
+ data = decoder.decode(message.toArray());
140
+ } else {
141
+ const arr = message.toArray();
142
+ data = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
143
+ }
144
+ const event = new MessageEvent("message", {
145
+ data,
146
+ origin: this.url
147
+ });
148
+ this.dispatchEvent(event);
149
+ if (this.onmessage) this.onmessage.call(this, event);
150
+ }
151
+ _onError(error) {
152
+ const event = new Event("error");
153
+ this.dispatchEvent(event);
154
+ if (this.onerror) this.onerror.call(this, event);
155
+ }
156
+ _onClosed() {
157
+ const code = this._connection?.get_close_code() ?? 1006;
158
+ const reason = this._connection?.get_close_data() ?? "";
159
+ const wasClean = code === 1e3;
160
+ this.readyState = CLOSED;
161
+ this._connection = null;
162
+ const event = new CloseEvent("close", {
163
+ code,
164
+ reason,
165
+ wasClean
166
+ });
167
+ this.dispatchEvent(event);
168
+ if (this.onclose) this.onclose.call(this, event);
169
+ }
170
+ /**
171
+ * Send data through the WebSocket connection.
172
+ *
173
+ * For strings, we intentionally route through `send_message(TEXT, bytes)`
174
+ * rather than the simpler `send_text(str)` API. Reason: `send_text()`
175
+ * takes a C `const char*` (null-terminated), so any embedded `\x00` in
176
+ * the JS string gets truncated at the first NUL at the GI boundary —
177
+ * Autobahn case 6.7.1 (single NUL in a text frame) was returned as an
178
+ * empty string. Going through `GLib.Bytes` preserves the exact byte
179
+ * sequence; Soup still sets the text-frame opcode because we pass
180
+ * `Soup.WebsocketDataType.TEXT` explicitly.
181
+ */
182
+ send(data) {
183
+ if (this.readyState !== OPEN) {
184
+ throw new DOMException("WebSocket is not open", "InvalidStateError");
185
+ }
186
+ if (!this._connection) return;
187
+ if (typeof data === "string") {
188
+ const bytes = new TextEncoder().encode(data);
189
+ this._connection.send_message(Soup.WebsocketDataType.TEXT, new GLib.Bytes(bytes));
190
+ } else {
191
+ let bytes;
192
+ if (data instanceof ArrayBuffer) {
193
+ bytes = new Uint8Array(data);
194
+ } else {
195
+ bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
196
+ }
197
+ this._connection.send_message(Soup.WebsocketDataType.BINARY, new GLib.Bytes(bytes));
198
+ }
199
+ }
200
+ /**
201
+ * Close the WebSocket connection.
202
+ */
203
+ close(code, reason) {
204
+ if (this.readyState === CLOSED || this.readyState === CLOSING) return;
205
+ if (code !== undefined && code !== 1e3 && (code < 3e3 || code > 4999)) {
206
+ throw new DOMException(`The code must be either 1000, or between 3000 and 4999. ${code} is neither.`, "InvalidAccessError");
207
+ }
208
+ this.readyState = CLOSING;
209
+ if (this._connection) {
210
+ this._connection.close(code ?? 1e3, reason ?? null);
211
+ } else {
212
+ this.readyState = CLOSED;
213
+ const event = new CloseEvent("close", {
214
+ code: 1006,
215
+ wasClean: false
216
+ });
217
+ this.dispatchEvent(event);
218
+ if (this.onclose) this.onclose.call(this, event);
219
+ }
220
+ }
216
221
  };
222
+
223
+ //#endregion
224
+ export { CloseEvent, MessageEvent, WebSocket, WebSocket as default };
@@ -1,10 +1,14 @@
1
- import { WebSocket, MessageEvent, CloseEvent } from "./index.js";
1
+ import { CloseEvent, MessageEvent, WebSocket } from "./index.js";
2
+
3
+ //#region src/register.ts
2
4
  if (typeof globalThis.WebSocket === "undefined") {
3
- globalThis.WebSocket = WebSocket;
5
+ globalThis.WebSocket = WebSocket;
4
6
  }
5
7
  if (typeof globalThis.MessageEvent === "undefined") {
6
- globalThis.MessageEvent = MessageEvent;
8
+ globalThis.MessageEvent = MessageEvent;
7
9
  }
8
10
  if (typeof globalThis.CloseEvent === "undefined") {
9
- globalThis.CloseEvent = CloseEvent;
11
+ globalThis.CloseEvent = CloseEvent;
10
12
  }
13
+
14
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/websocket",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "W3C WebSocket API for GJS using Soup 3.0",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -38,16 +38,16 @@
38
38
  "soup"
39
39
  ],
40
40
  "devDependencies": {
41
- "@gjsify/cli": "^0.3.13",
42
- "@gjsify/unit": "^0.3.13",
41
+ "@gjsify/cli": "^0.3.15",
42
+ "@gjsify/unit": "^0.3.15",
43
43
  "@types/node": "^25.6.0",
44
44
  "typescript": "^6.0.3"
45
45
  },
46
46
  "dependencies": {
47
- "@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
48
- "@girs/gjs": "^4.0.0-rc.9",
49
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
50
- "@girs/soup-3.0": "^3.6.6-4.0.0-rc.9",
51
- "@gjsify/dom-events": "^0.3.13"
47
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.9",
48
+ "@girs/gjs": "4.0.0-rc.9",
49
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
50
+ "@girs/soup-3.0": "3.6.6-4.0.0-rc.9",
51
+ "@gjsify/dom-events": "^0.3.15"
52
52
  }
53
53
  }