@gjsify/ws 0.3.12 → 0.3.14

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.
@@ -1,249 +1,276 @@
1
+ import { BINARY_TYPES, CLOSED, CLOSING, CONNECTING, OPEN } from "./constants.js";
1
2
  import { EventEmitter } from "@gjsify/events";
2
3
  import { Buffer } from "@gjsify/buffer";
3
- import { WebSocket as NativeWebSocket } from "@gjsify/websocket";
4
- import {
5
- BINARY_TYPES,
6
- CLOSED,
7
- CLOSING,
8
- CONNECTING,
9
- OPEN
10
- } from "./constants.js";
11
- class WebSocket extends EventEmitter {
12
- /** Static readyState constants — match the W3C and `ws` values. */
13
- static CONNECTING = CONNECTING;
14
- static OPEN = OPEN;
15
- static CLOSING = CLOSING;
16
- static CLOSED = CLOSED;
17
- /** Instance-side copies for `ws.readyState === ws.OPEN` style code. */
18
- CONNECTING = CONNECTING;
19
- OPEN = OPEN;
20
- CLOSING = CLOSING;
21
- CLOSED = CLOSED;
22
- /** Per-instance state. Populated from the underlying native WebSocket as
23
- * events arrive; exposed as properties so that ws-calling code like
24
- * `if (ws.readyState === ws.OPEN)` works identically to Node's `ws`. */
25
- readyState = CONNECTING;
26
- url = "";
27
- protocol = "";
28
- extensions = "";
29
- bufferedAmount = 0;
30
- binaryType = "nodebuffer";
31
- /** The real WebSocket we delegate to. Typed as `any` because the W3C
32
- * ambient type comes from multiple realms depending on where this bundle
33
- * ends up (GJS browser-like globals vs. Node's undici). */
34
- _native = null;
35
- constructor(address, protocols, options = {}) {
36
- super();
37
- if (address === null) {
38
- queueMicrotask(() => this._fail("Constructing ws.WebSocket with null address is not supported on Gjs"));
39
- return;
40
- }
41
- this.url = typeof address === "string" ? address : String(address);
42
- const protos = this._resolveProtocols(protocols, options);
43
- this._openNative(this.url, protos, options);
44
- }
45
- /** Merge `protocols` arg and `options.protocols` / `options.protocol`. */
46
- _resolveProtocols(protocols, options) {
47
- if (protocols !== void 0) {
48
- return Array.isArray(protocols) ? protocols : [protocols];
49
- }
50
- if (options.protocols !== void 0) {
51
- return Array.isArray(options.protocols) ? options.protocols : [options.protocols];
52
- }
53
- if (options.protocol !== void 0) return [options.protocol];
54
- return void 0;
55
- }
56
- /** Lazy-open the underlying native WebSocket. Separated from the constructor
57
- * so subclasses or future socket-adoption paths can bypass it. */
58
- _openNative(url, protocols, options) {
59
- if (typeof NativeWebSocket !== "function") {
60
- queueMicrotask(() => this._fail(
61
- "@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."
62
- ));
63
- return;
64
- }
65
- const nativeOpts = {
66
- perMessageDeflate: options.perMessageDeflate !== false,
67
- headers: options.headers,
68
- origin: options.origin,
69
- handshakeTimeout: options.handshakeTimeout
70
- };
71
- try {
72
- this._native = new NativeWebSocket(url, protocols, nativeOpts);
73
- } catch (err) {
74
- queueMicrotask(() => this._fail(err instanceof Error ? err : new Error(String(err))));
75
- return;
76
- }
77
- this._native.binaryType = "arraybuffer";
78
- this._native.addEventListener("open", () => this._onOpen());
79
- this._native.addEventListener("message", (ev) => this._onMessage(ev));
80
- this._native.addEventListener("close", (ev) => this._onClose(ev));
81
- this._native.addEventListener("error", (ev) => this._onError(ev));
82
- }
83
- _fail(err) {
84
- const error = typeof err === "string" ? new Error(err) : err;
85
- this.readyState = CLOSED;
86
- this.emit("error", error);
87
- this._dispatchEvent("error", { error, message: error.message });
88
- this.emit("close", 1006, Buffer.from(error.message));
89
- this._dispatchEvent("close", { code: 1006, reason: error.message, wasClean: false });
90
- }
91
- _onOpen() {
92
- this.readyState = OPEN;
93
- if (typeof this._native.protocol === "string") this.protocol = this._native.protocol;
94
- if (typeof this._native.extensions === "string") this.extensions = this._native.extensions;
95
- this.emit("open");
96
- this._dispatchEvent("open", {});
97
- }
98
- _onMessage(ev) {
99
- const raw = ev?.data;
100
- let data;
101
- let isBinary = false;
102
- if (typeof raw === "string") {
103
- data = raw;
104
- isBinary = false;
105
- } else if (raw instanceof ArrayBuffer) {
106
- isBinary = true;
107
- data = this._decodeBinary(raw);
108
- } else if (ArrayBuffer.isView(raw)) {
109
- isBinary = true;
110
- data = this._decodeBinary(raw.buffer);
111
- } else {
112
- data = raw;
113
- isBinary = false;
114
- }
115
- this.emit("message", data, isBinary);
116
- this._dispatchEvent("message", { data, type: isBinary ? "binary" : "text" });
117
- }
118
- /** Convert an ArrayBuffer to the Buffer flavor requested by `binaryType`. */
119
- _decodeBinary(buf) {
120
- switch (this.binaryType) {
121
- case "arraybuffer":
122
- return buf;
123
- case "fragments":
124
- return [Buffer.from(buf)];
125
- // one-fragment approximation
126
- case "blob": {
127
- const BlobCtor = globalThis.Blob;
128
- return BlobCtor ? new BlobCtor([new Uint8Array(buf)]) : Buffer.from(buf);
129
- }
130
- case "nodebuffer":
131
- default:
132
- return Buffer.from(buf);
133
- }
134
- }
135
- _onClose(ev) {
136
- const code = typeof ev?.code === "number" ? ev.code : 1006;
137
- const reason = typeof ev?.reason === "string" ? ev.reason : "";
138
- this.readyState = CLOSED;
139
- this.emit("close", code, Buffer.from(reason));
140
- this._dispatchEvent("close", { code, reason, wasClean: !!ev?.wasClean });
141
- }
142
- _onError(ev) {
143
- const msg = ev?.message || "WebSocket error";
144
- const err = ev?.error instanceof Error ? ev.error : new Error(msg);
145
- this.emit("error", err);
146
- this._dispatchEvent("error", { error: err, message: msg });
147
- }
148
- send(data, optionsOrCb, cb) {
149
- const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
150
- if (this.readyState === CONNECTING) {
151
- throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
152
- }
153
- if (this.readyState !== OPEN) {
154
- const err = new Error("WebSocket is not open: readyState " + this.readyState);
155
- if (callback) queueMicrotask(() => callback(err));
156
- else queueMicrotask(() => this.emit("error", err));
157
- return;
158
- }
159
- this._nativeSend(data, callback);
160
- }
161
- _nativeSend(data, cb) {
162
- try {
163
- let payload = data;
164
- if (typeof data === "number" || typeof data === "boolean") {
165
- payload = String(data);
166
- } else if (Buffer.isBuffer(data)) {
167
- const b = data;
168
- payload = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
169
- }
170
- this._native.send(payload);
171
- if (cb) queueMicrotask(() => cb());
172
- } catch (err) {
173
- const e = err instanceof Error ? err : new Error(String(err));
174
- if (cb) queueMicrotask(() => cb(e));
175
- else queueMicrotask(() => this.emit("error", e));
176
- }
177
- }
178
- _sizeOf(data) {
179
- if (typeof data === "string") return data.length;
180
- if (data instanceof ArrayBuffer) return data.byteLength;
181
- if (ArrayBuffer.isView(data)) return data.byteLength;
182
- return 0;
183
- }
184
- close(code, reason) {
185
- if (this.readyState === CLOSED) return;
186
- if (this.readyState === CLOSING) return;
187
- this.readyState = CLOSING;
188
- try {
189
- const reasonStr = reason === void 0 ? void 0 : Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
190
- if (code === void 0) this._native?.close();
191
- else if (reasonStr === void 0) this._native?.close(code);
192
- else this._native?.close(code, reasonStr);
193
- } catch (err) {
194
- this.emit("error", err instanceof Error ? err : new Error(String(err)));
195
- }
196
- }
197
- /** ws-only: force-close without sending a Close frame. On Gjs we can't bypass
198
- * Soup's close handshake, so terminate is approximated as close(1006).
199
- * Known gap vs. ws semantics — documented. */
200
- terminate() {
201
- if (this.readyState === CLOSED) return;
202
- this.readyState = CLOSING;
203
- try {
204
- this._native?.close(1006, "terminated");
205
- } catch {
206
- }
207
- }
208
- /** Convenience: returns true if the socket is closed or closing. Matches
209
- * ws.isPaused() / internal state checks that some consumers rely on. */
210
- get isPaused() {
211
- return this.readyState === CLOSING || this.readyState === CLOSED;
212
- }
213
- // --- W3C EventTarget compat surface -------------------------------------
214
- // ws uses EventEmitter by default, but exposes addEventListener-style
215
- // registration for consumers that prefer the W3C API. We mirror it to
216
- // stay compat.
217
- _eventTargetListeners = /* @__PURE__ */ new Map();
218
- addEventListener(type, listener) {
219
- let set = this._eventTargetListeners.get(type);
220
- if (!set) {
221
- set = /* @__PURE__ */ new Set();
222
- this._eventTargetListeners.set(type, set);
223
- }
224
- set.add(listener);
225
- }
226
- removeEventListener(type, listener) {
227
- this._eventTargetListeners.get(type)?.delete(listener);
228
- }
229
- _dispatchEvent(type, detail) {
230
- const set = this._eventTargetListeners.get(type);
231
- if (!set || set.size === 0) return;
232
- const ev = Object.assign({ type, target: this }, detail);
233
- for (const listener of set) {
234
- try {
235
- listener(ev);
236
- } catch (err) {
237
- queueMicrotask(() => this.emit("error", err));
238
- }
239
- }
240
- }
241
- // Static list of accepted binary types — exposed for parity with ws.
242
- static get BINARY_TYPES() {
243
- return BINARY_TYPES;
244
- }
245
- }
246
- WebSocket.WebSocket = WebSocket;
247
- export {
248
- WebSocket
4
+ import { WebSocket as WebSocket$1 } from "@gjsify/websocket";
5
+
6
+ //#region src/websocket.ts
7
+ /** `ws.WebSocket` — EventEmitter-based WebSocket client.
8
+ *
9
+ * Events (ws-compatible):
10
+ * - 'open' → ()
11
+ * - 'message' → (data: Buffer | ArrayBuffer | string, isBinary: boolean)
12
+ * - 'close' → (code: number, reason: Buffer)
13
+ * - 'error' → (error: Error)
14
+ * - 'ping' / 'pong' → NOT EMITTED on Gjs (Soup handles control
15
+ * frames internally; no JS hook exposed).
16
+ * Consumers that rely on 'ping'/'pong' for
17
+ * keep-alive application logic need to use
18
+ * data messages instead. Tracked as a
19
+ * known limitation in STATUS.md.
20
+ * - 'upgrade' / 'unexpected-response' / 'redirect'
21
+ * → NOT EMITTED on Gjs (Soup does not expose
22
+ * the raw HTTP upgrade response). Code
23
+ * that only branches on `open` vs `error`
24
+ * still works.
25
+ *
26
+ * Also implements W3C DOM `EventTarget` methods (addEventListener /
27
+ * removeEventListener) so `ws` users who follow W3C-style handlers via the
28
+ * `ws.EventTarget` flag still work without special-casing the Gjs path.
29
+ */
30
+ var WebSocket = class extends EventEmitter {
31
+ /** Static readyState constants — match the W3C and `ws` values. */
32
+ static CONNECTING = 0;
33
+ static OPEN = 1;
34
+ static CLOSING = 2;
35
+ static CLOSED = 3;
36
+ /** Instance-side copies for `ws.readyState === ws.OPEN` style code. */
37
+ CONNECTING = 0;
38
+ OPEN = 1;
39
+ CLOSING = 2;
40
+ CLOSED = 3;
41
+ /** Per-instance state. Populated from the underlying native WebSocket as
42
+ * events arrive; exposed as properties so that ws-calling code like
43
+ * `if (ws.readyState === ws.OPEN)` works identically to Node's `ws`. */
44
+ readyState = 0;
45
+ url = "";
46
+ protocol = "";
47
+ extensions = "";
48
+ bufferedAmount = 0;
49
+ binaryType = "nodebuffer";
50
+ /** The real WebSocket we delegate to. Typed as `any` because the W3C
51
+ * ambient type comes from multiple realms depending on where this bundle
52
+ * ends up (GJS browser-like globals vs. Node's undici). */
53
+ _native = null;
54
+ constructor(address, protocols, options = {}) {
55
+ super();
56
+ if (address === null) {
57
+ queueMicrotask(() => this._fail("Constructing ws.WebSocket with null address is not supported on Gjs"));
58
+ return;
59
+ }
60
+ this.url = typeof address === "string" ? address : String(address);
61
+ const protos = this._resolveProtocols(protocols, options);
62
+ this._openNative(this.url, protos, options);
63
+ }
64
+ /** Merge `protocols` arg and `options.protocols` / `options.protocol`. */
65
+ _resolveProtocols(protocols, options) {
66
+ if (protocols !== undefined) {
67
+ return Array.isArray(protocols) ? protocols : [protocols];
68
+ }
69
+ if (options.protocols !== undefined) {
70
+ return Array.isArray(options.protocols) ? options.protocols : [options.protocols];
71
+ }
72
+ if (options.protocol !== undefined) return [options.protocol];
73
+ return undefined;
74
+ }
75
+ /** Lazy-open the underlying native WebSocket. Separated from the constructor
76
+ * so subclasses or future socket-adoption paths can bypass it. */
77
+ _openNative(url, protocols, options) {
78
+ if (typeof WebSocket$1 !== "function") {
79
+ 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."));
80
+ return;
81
+ }
82
+ const nativeOpts = {
83
+ perMessageDeflate: options.perMessageDeflate !== false,
84
+ headers: options.headers,
85
+ origin: options.origin,
86
+ handshakeTimeout: options.handshakeTimeout
87
+ };
88
+ try {
89
+ this._native = new WebSocket$1(url, protocols, nativeOpts);
90
+ } catch (err) {
91
+ queueMicrotask(() => this._fail(err instanceof Error ? err : new Error(String(err))));
92
+ return;
93
+ }
94
+ this._native.binaryType = "arraybuffer";
95
+ this._native.addEventListener("open", () => this._onOpen());
96
+ this._native.addEventListener("message", (ev) => this._onMessage(ev));
97
+ this._native.addEventListener("close", (ev) => this._onClose(ev));
98
+ this._native.addEventListener("error", (ev) => this._onError(ev));
99
+ }
100
+ _fail(err) {
101
+ const error = typeof err === "string" ? new Error(err) : err;
102
+ this.readyState = 3;
103
+ this.emit("error", error);
104
+ this._dispatchEvent("error", {
105
+ error,
106
+ message: error.message
107
+ });
108
+ this.emit("close", 1006, Buffer.from(error.message));
109
+ this._dispatchEvent("close", {
110
+ code: 1006,
111
+ reason: error.message,
112
+ wasClean: false
113
+ });
114
+ }
115
+ _onOpen() {
116
+ this.readyState = 1;
117
+ if (typeof this._native.protocol === "string") this.protocol = this._native.protocol;
118
+ if (typeof this._native.extensions === "string") this.extensions = this._native.extensions;
119
+ this.emit("open");
120
+ this._dispatchEvent("open", {});
121
+ }
122
+ _onMessage(ev) {
123
+ const raw = ev?.data;
124
+ let data;
125
+ let isBinary = false;
126
+ if (typeof raw === "string") {
127
+ data = raw;
128
+ isBinary = false;
129
+ } else if (raw instanceof ArrayBuffer) {
130
+ isBinary = true;
131
+ data = this._decodeBinary(raw);
132
+ } else if (ArrayBuffer.isView(raw)) {
133
+ isBinary = true;
134
+ data = this._decodeBinary(raw.buffer);
135
+ } else {
136
+ data = raw;
137
+ isBinary = false;
138
+ }
139
+ this.emit("message", data, isBinary);
140
+ this._dispatchEvent("message", {
141
+ data,
142
+ type: isBinary ? "binary" : "text"
143
+ });
144
+ }
145
+ /** Convert an ArrayBuffer to the Buffer flavor requested by `binaryType`. */
146
+ _decodeBinary(buf) {
147
+ switch (this.binaryType) {
148
+ case "arraybuffer": return buf;
149
+ case "fragments": return [Buffer.from(buf)];
150
+ case "blob": {
151
+ const BlobCtor = globalThis.Blob;
152
+ return BlobCtor ? new BlobCtor([new Uint8Array(buf)]) : Buffer.from(buf);
153
+ }
154
+ case "nodebuffer":
155
+ default: return Buffer.from(buf);
156
+ }
157
+ }
158
+ _onClose(ev) {
159
+ const code = typeof ev?.code === "number" ? ev.code : 1006;
160
+ const reason = typeof ev?.reason === "string" ? ev.reason : "";
161
+ this.readyState = 3;
162
+ this.emit("close", code, Buffer.from(reason));
163
+ this._dispatchEvent("close", {
164
+ code,
165
+ reason,
166
+ wasClean: !!ev?.wasClean
167
+ });
168
+ }
169
+ _onError(ev) {
170
+ const msg = ev?.message || "WebSocket error";
171
+ const err = ev?.error instanceof Error ? ev.error : new Error(msg);
172
+ this.emit("error", err);
173
+ this._dispatchEvent("error", {
174
+ error: err,
175
+ message: msg
176
+ });
177
+ }
178
+ send(data, optionsOrCb, cb) {
179
+ const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
180
+ if (this.readyState === 0) {
181
+ throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
182
+ }
183
+ if (this.readyState !== 1) {
184
+ const err = new Error("WebSocket is not open: readyState " + this.readyState);
185
+ if (callback) queueMicrotask(() => callback(err));
186
+ else queueMicrotask(() => this.emit("error", err));
187
+ return;
188
+ }
189
+ this._nativeSend(data, callback);
190
+ }
191
+ _nativeSend(data, cb) {
192
+ try {
193
+ let payload = data;
194
+ if (typeof data === "number" || typeof data === "boolean") {
195
+ payload = String(data);
196
+ } else if (Buffer.isBuffer(data)) {
197
+ const b = data;
198
+ payload = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
199
+ }
200
+ this._native.send(payload);
201
+ if (cb) queueMicrotask(() => cb());
202
+ } catch (err) {
203
+ const e = err instanceof Error ? err : new Error(String(err));
204
+ if (cb) queueMicrotask(() => cb(e));
205
+ else queueMicrotask(() => this.emit("error", e));
206
+ }
207
+ }
208
+ _sizeOf(data) {
209
+ if (typeof data === "string") return data.length;
210
+ if (data instanceof ArrayBuffer) return data.byteLength;
211
+ if (ArrayBuffer.isView(data)) return data.byteLength;
212
+ return 0;
213
+ }
214
+ close(code, reason) {
215
+ if (this.readyState === 3) return;
216
+ if (this.readyState === 2) return;
217
+ this.readyState = 2;
218
+ try {
219
+ const reasonStr = reason === undefined ? undefined : Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
220
+ if (code === undefined) this._native?.close();
221
+ else if (reasonStr === undefined) this._native?.close(code);
222
+ else this._native?.close(code, reasonStr);
223
+ } catch (err) {
224
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
225
+ }
226
+ }
227
+ /** ws-only: force-close without sending a Close frame. On Gjs we can't bypass
228
+ * Soup's close handshake, so terminate is approximated as close(1006).
229
+ * Known gap vs. ws semantics — documented. */
230
+ terminate() {
231
+ if (this.readyState === 3) return;
232
+ this.readyState = 2;
233
+ try {
234
+ this._native?.close(1006, "terminated");
235
+ } catch {}
236
+ }
237
+ /** Convenience: returns true if the socket is closed or closing. Matches
238
+ * ws.isPaused() / internal state checks that some consumers rely on. */
239
+ get isPaused() {
240
+ return this.readyState === 2 || this.readyState === 3;
241
+ }
242
+ _eventTargetListeners = new Map();
243
+ addEventListener(type, listener) {
244
+ let set = this._eventTargetListeners.get(type);
245
+ if (!set) {
246
+ set = new Set();
247
+ this._eventTargetListeners.set(type, set);
248
+ }
249
+ set.add(listener);
250
+ }
251
+ removeEventListener(type, listener) {
252
+ this._eventTargetListeners.get(type)?.delete(listener);
253
+ }
254
+ _dispatchEvent(type, detail) {
255
+ const set = this._eventTargetListeners.get(type);
256
+ if (!set || set.size === 0) return;
257
+ const ev = Object.assign({
258
+ type,
259
+ target: this
260
+ }, detail);
261
+ for (const listener of set) {
262
+ try {
263
+ listener(ev);
264
+ } catch (err) {
265
+ queueMicrotask(() => this.emit("error", err));
266
+ }
267
+ }
268
+ }
269
+ static get BINARY_TYPES() {
270
+ return BINARY_TYPES;
271
+ }
249
272
  };
273
+ WebSocket.WebSocket = WebSocket;
274
+
275
+ //#endregion
276
+ export { WebSocket };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/ws",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Drop-in replacement for the `ws` npm package on Gjs — wraps globalThis.WebSocket (Soup.WebsocketConnection) and Soup.Server for the WebSocketServer side",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -31,18 +31,18 @@
31
31
  "websocket"
32
32
  ],
33
33
  "dependencies": {
34
- "@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
35
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
36
- "@girs/soup-3.0": "^3.6.6-4.0.0-rc.9",
37
- "@gjsify/buffer": "^0.3.12",
38
- "@gjsify/crypto": "^0.3.12",
39
- "@gjsify/events": "^0.3.12",
40
- "@gjsify/utils": "^0.3.12",
41
- "@gjsify/websocket": "^0.3.12"
34
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.9",
35
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
36
+ "@girs/soup-3.0": "3.6.6-4.0.0-rc.9",
37
+ "@gjsify/buffer": "^0.3.14",
38
+ "@gjsify/crypto": "^0.3.14",
39
+ "@gjsify/events": "^0.3.14",
40
+ "@gjsify/utils": "^0.3.14",
41
+ "@gjsify/websocket": "^0.3.14"
42
42
  },
43
43
  "devDependencies": {
44
- "@gjsify/cli": "^0.3.12",
45
- "@gjsify/unit": "^0.3.12",
44
+ "@gjsify/cli": "^0.3.14",
45
+ "@gjsify/unit": "^0.3.14",
46
46
  "@types/node": "^25.6.0",
47
47
  "typescript": "^6.0.3"
48
48
  }