@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 +212 -204
- package/lib/esm/register.js +8 -4
- package/package.json +8 -8
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 };
|
package/lib/esm/register.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CloseEvent, MessageEvent, WebSocket } from "./index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/register.ts
|
|
2
4
|
if (typeof globalThis.WebSocket === "undefined") {
|
|
3
|
-
|
|
5
|
+
globalThis.WebSocket = WebSocket;
|
|
4
6
|
}
|
|
5
7
|
if (typeof globalThis.MessageEvent === "undefined") {
|
|
6
|
-
|
|
8
|
+
globalThis.MessageEvent = MessageEvent;
|
|
7
9
|
}
|
|
8
10
|
if (typeof globalThis.CloseEvent === "undefined") {
|
|
9
|
-
|
|
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.
|
|
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.
|
|
42
|
-
"@gjsify/unit": "^0.3.
|
|
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": "
|
|
48
|
-
"@girs/gjs": "
|
|
49
|
-
"@girs/glib-2.0": "
|
|
50
|
-
"@girs/soup-3.0": "
|
|
51
|
-
"@gjsify/dom-events": "^0.3.
|
|
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
|
}
|