@gjsify/ws 0.2.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/lib/esm/constants.js +16 -0
- package/lib/esm/index.js +15 -0
- package/lib/esm/stream.js +89 -0
- package/lib/esm/websocket-server.js +409 -0
- package/lib/esm/websocket.js +249 -0
- package/lib/types/constants.d.ts +9 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/index.spec.d.ts +2 -0
- package/lib/types/stream.d.ts +2 -0
- package/lib/types/stream.spec.d.ts +2 -0
- package/lib/types/websocket-server.d.ts +119 -0
- package/lib/types/websocket-server.spec.d.ts +2 -0
- package/lib/types/websocket.d.ts +99 -0
- package/package.json +49 -0
- package/src/constants.ts +15 -0
- package/src/index.spec.ts +144 -0
- package/src/index.ts +25 -0
- package/src/stream.spec.ts +121 -0
- package/src/stream.ts +107 -0
- package/src/test.mts +5 -0
- package/src/websocket-server.spec.ts +354 -0
- package/src/websocket-server.ts +561 -0
- package/src/websocket.ts +391 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { EventEmitter } from "@gjsify/events";
|
|
2
|
+
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
|
|
249
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const BINARY_TYPES: readonly ["nodebuffer", "arraybuffer", "fragments"];
|
|
2
|
+
export declare const EMPTY_BUFFER: Uint8Array<ArrayBuffer>;
|
|
3
|
+
export declare const CONNECTING = 0;
|
|
4
|
+
export declare const OPEN = 1;
|
|
5
|
+
export declare const CLOSING = 2;
|
|
6
|
+
export declare const CLOSED = 3;
|
|
7
|
+
/** Internal marker for native `globalThis.WebSocket` instances handed in via
|
|
8
|
+
* `{ socket }` option (not currently supported but reserved). */
|
|
9
|
+
export declare const kWebSocket: unique symbol;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WebSocket } from './websocket.js';
|
|
2
|
+
import { WebSocketServer } from './websocket-server.js';
|
|
3
|
+
import { createWebSocketStream } from './stream.js';
|
|
4
|
+
export { WebSocket, WebSocketServer, createWebSocketStream };
|
|
5
|
+
export { WebSocketServer as Server };
|
|
6
|
+
export default WebSocket;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { EventEmitter } from '@gjsify/events';
|
|
2
|
+
import { Buffer } from '@gjsify/buffer';
|
|
3
|
+
import Soup from '@girs/soup-3.0';
|
|
4
|
+
/** Structural duck-type for @gjsify/http Server — avoids a hard dep on @gjsify/http. */
|
|
5
|
+
interface HttpServer {
|
|
6
|
+
soupServer: Soup.Server | null;
|
|
7
|
+
address(): {
|
|
8
|
+
address: string;
|
|
9
|
+
family: string;
|
|
10
|
+
port: number;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
export interface VerifyClientInfo {
|
|
14
|
+
/** Value of the HTTP Origin request header (empty string if absent). */
|
|
15
|
+
origin: string;
|
|
16
|
+
/** Whether the connection uses TLS. Always false on Gjs (Soup plain text). */
|
|
17
|
+
secure: boolean;
|
|
18
|
+
/** Minimal HTTP request object populated from Soup.ServerMessage. */
|
|
19
|
+
req: {
|
|
20
|
+
method: string;
|
|
21
|
+
url: string;
|
|
22
|
+
headers: Record<string, string | string[]>;
|
|
23
|
+
socket: {
|
|
24
|
+
remoteAddress: string;
|
|
25
|
+
remotePort: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export type VerifyClientSync = (info: VerifyClientInfo) => boolean;
|
|
30
|
+
export type VerifyClientAsync = (info: VerifyClientInfo, cb: (result: boolean, code?: number, message?: string, headers?: Record<string, string>) => void) => void;
|
|
31
|
+
export interface ServerOptions {
|
|
32
|
+
host?: string;
|
|
33
|
+
port?: number;
|
|
34
|
+
backlog?: number;
|
|
35
|
+
/** Attach to an existing @gjsify/http Server instead of creating a new one. */
|
|
36
|
+
server?: HttpServer;
|
|
37
|
+
/** Pre-upgrade access control hook. Sync: return boolean. Async: call cb(result, code?). */
|
|
38
|
+
verifyClient?: VerifyClientSync | VerifyClientAsync;
|
|
39
|
+
/** Subprotocol selection hook. Receives the Set of client-offered protocols and
|
|
40
|
+
* a minimal request object; return the selected protocol string or false to
|
|
41
|
+
* use none. Server-side ws.protocol is set correctly; client-visible protocol
|
|
42
|
+
* negotiation requires Phase 3 (manual handshake). */
|
|
43
|
+
handleProtocols?: (protocols: Set<string>, req: VerifyClientInfo['req']) => string | false;
|
|
44
|
+
path?: string;
|
|
45
|
+
noServer?: boolean;
|
|
46
|
+
clientTracking?: boolean;
|
|
47
|
+
perMessageDeflate?: boolean | object;
|
|
48
|
+
maxPayload?: number;
|
|
49
|
+
skipUTF8Validation?: boolean;
|
|
50
|
+
allowSynchronousEvents?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Wraps an accepted Soup.WebsocketConnection in a `ws.WebSocket`-shaped
|
|
53
|
+
* EventEmitter. Kept private to this file: the WebSocket class in
|
|
54
|
+
* ./websocket.ts targets the CLIENT constructor path. Server-accepted
|
|
55
|
+
* connections have different semantics (no URL reconnect, different
|
|
56
|
+
* lifecycle) so we expose a narrower surface. */
|
|
57
|
+
declare class ServerSideWebSocket extends EventEmitter {
|
|
58
|
+
static readonly CONNECTING = 0;
|
|
59
|
+
static readonly OPEN = 1;
|
|
60
|
+
static readonly CLOSING = 2;
|
|
61
|
+
static readonly CLOSED = 3;
|
|
62
|
+
readonly CONNECTING = 0;
|
|
63
|
+
readonly OPEN = 1;
|
|
64
|
+
readonly CLOSING = 2;
|
|
65
|
+
readonly CLOSED = 3;
|
|
66
|
+
readyState: number;
|
|
67
|
+
protocol: string;
|
|
68
|
+
extensions: string;
|
|
69
|
+
url: string;
|
|
70
|
+
private _conn;
|
|
71
|
+
constructor(conn: Soup.WebsocketConnection, url: string);
|
|
72
|
+
send(data: string | Buffer | ArrayBuffer | ArrayBufferView, optionsOrCb?: ((err?: Error) => void) | object, cb?: (err?: Error) => void): void;
|
|
73
|
+
close(code?: number, reason?: string | Buffer): void;
|
|
74
|
+
terminate(): void;
|
|
75
|
+
}
|
|
76
|
+
/** `ws.WebSocketServer` — listens on a TCP port (or attaches to an existing
|
|
77
|
+
* @gjsify/http Server) and emits 'connection' events wrapping
|
|
78
|
+
* Soup.WebsocketConnection as ws.WebSocket-shaped objects. */
|
|
79
|
+
export declare class WebSocketServer extends EventEmitter {
|
|
80
|
+
readonly options: ServerOptions;
|
|
81
|
+
readonly clients: Set<ServerSideWebSocket>;
|
|
82
|
+
readonly path: string;
|
|
83
|
+
private _server;
|
|
84
|
+
private _address;
|
|
85
|
+
constructor(options?: ServerOptions, callback?: () => void);
|
|
86
|
+
private _buildVerifyClientInfo;
|
|
87
|
+
/** Register add_handler (verifyClient) + add_websocket_handler on soupServer.
|
|
88
|
+
* The verifyClient add_handler MUST be registered before add_websocket_handler —
|
|
89
|
+
* Soup processes normal handlers before websocket handlers; setting a status code
|
|
90
|
+
* in add_handler prevents the websocket handler from firing (HTTP-level rejection).
|
|
91
|
+
* Only register add_handler when verifyClient is provided — a no-op handler on the
|
|
92
|
+
* same path as an existing http.Server catch-all can interfere with Soup's routing. */
|
|
93
|
+
private _setupHandlers;
|
|
94
|
+
private _start;
|
|
95
|
+
address(): {
|
|
96
|
+
address: string;
|
|
97
|
+
family: string;
|
|
98
|
+
port: number;
|
|
99
|
+
} | null;
|
|
100
|
+
close(callback?: (err?: Error) => void): void;
|
|
101
|
+
/** Manual WebSocket upgrade — matches npm ws semantics exactly.
|
|
102
|
+
* The caller intercepts 'upgrade' on an http.Server (typically with
|
|
103
|
+
* { noServer: true } on this WebSocketServer) and passes the raw
|
|
104
|
+
* IncomingMessage + net.Socket + head buffer here.
|
|
105
|
+
*
|
|
106
|
+
* Internally: validates headers, runs verifyClient, computes
|
|
107
|
+
* Sec-WebSocket-Accept, emits 'headers' (mutable array), writes the 101
|
|
108
|
+
* response via socket.write(), then creates Soup.WebsocketConnection from
|
|
109
|
+
* the underlying IOStream and calls cb(ws, req). */
|
|
110
|
+
handleUpgrade(req: any, socket: any, _head: Buffer, cb: (ws: ServerSideWebSocket, req: any) => void): void;
|
|
111
|
+
shouldHandle(req: {
|
|
112
|
+
url?: string;
|
|
113
|
+
}): boolean;
|
|
114
|
+
private _validateUpgradeHeaders;
|
|
115
|
+
private _completeUpgrade;
|
|
116
|
+
private _abortHandshake;
|
|
117
|
+
private _buildVerifyClientInfoFromReq;
|
|
118
|
+
}
|
|
119
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EventEmitter } from '@gjsify/events';
|
|
2
|
+
import { Buffer } from '@gjsify/buffer';
|
|
3
|
+
export type BinaryType = 'nodebuffer' | 'arraybuffer' | 'fragments' | 'blob';
|
|
4
|
+
/** Options accepted by the ws constructor. Only a subset is honored on Gjs —
|
|
5
|
+
* see README for the support matrix. Unknown options are silently ignored
|
|
6
|
+
* to preserve drop-in compatibility with `ws`-calling code. */
|
|
7
|
+
export interface ClientOptions {
|
|
8
|
+
protocol?: string;
|
|
9
|
+
protocols?: string | string[];
|
|
10
|
+
origin?: string;
|
|
11
|
+
headers?: Record<string, string | string[]>;
|
|
12
|
+
handshakeTimeout?: number;
|
|
13
|
+
/** Enable permessage-deflate (RFC 7692). Defaults to true, matching the real
|
|
14
|
+
* ws npm package. Set to false to disable deflate negotiation (useful when
|
|
15
|
+
* the remote server has buggy deflate handling). */
|
|
16
|
+
perMessageDeflate?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** `ws.WebSocket` — EventEmitter-based WebSocket client.
|
|
19
|
+
*
|
|
20
|
+
* Events (ws-compatible):
|
|
21
|
+
* - 'open' → ()
|
|
22
|
+
* - 'message' → (data: Buffer | ArrayBuffer | string, isBinary: boolean)
|
|
23
|
+
* - 'close' → (code: number, reason: Buffer)
|
|
24
|
+
* - 'error' → (error: Error)
|
|
25
|
+
* - 'ping' / 'pong' → NOT EMITTED on Gjs (Soup handles control
|
|
26
|
+
* frames internally; no JS hook exposed).
|
|
27
|
+
* Consumers that rely on 'ping'/'pong' for
|
|
28
|
+
* keep-alive application logic need to use
|
|
29
|
+
* data messages instead. Tracked as a
|
|
30
|
+
* known limitation in STATUS.md.
|
|
31
|
+
* - 'upgrade' / 'unexpected-response' / 'redirect'
|
|
32
|
+
* → NOT EMITTED on Gjs (Soup does not expose
|
|
33
|
+
* the raw HTTP upgrade response). Code
|
|
34
|
+
* that only branches on `open` vs `error`
|
|
35
|
+
* still works.
|
|
36
|
+
*
|
|
37
|
+
* Also implements W3C DOM `EventTarget` methods (addEventListener /
|
|
38
|
+
* removeEventListener) so `ws` users who follow W3C-style handlers via the
|
|
39
|
+
* `ws.EventTarget` flag still work without special-casing the Gjs path.
|
|
40
|
+
*/
|
|
41
|
+
export declare class WebSocket extends EventEmitter {
|
|
42
|
+
/** Static readyState constants — match the W3C and `ws` values. */
|
|
43
|
+
static readonly CONNECTING = 0;
|
|
44
|
+
static readonly OPEN = 1;
|
|
45
|
+
static readonly CLOSING = 2;
|
|
46
|
+
static readonly CLOSED = 3;
|
|
47
|
+
/** Instance-side copies for `ws.readyState === ws.OPEN` style code. */
|
|
48
|
+
readonly CONNECTING = 0;
|
|
49
|
+
readonly OPEN = 1;
|
|
50
|
+
readonly CLOSING = 2;
|
|
51
|
+
readonly CLOSED = 3;
|
|
52
|
+
/** Per-instance state. Populated from the underlying native WebSocket as
|
|
53
|
+
* events arrive; exposed as properties so that ws-calling code like
|
|
54
|
+
* `if (ws.readyState === ws.OPEN)` works identically to Node's `ws`. */
|
|
55
|
+
readyState: number;
|
|
56
|
+
url: string;
|
|
57
|
+
protocol: string;
|
|
58
|
+
extensions: string;
|
|
59
|
+
bufferedAmount: number;
|
|
60
|
+
binaryType: BinaryType;
|
|
61
|
+
/** The real WebSocket we delegate to. Typed as `any` because the W3C
|
|
62
|
+
* ambient type comes from multiple realms depending on where this bundle
|
|
63
|
+
* ends up (GJS browser-like globals vs. Node's undici). */
|
|
64
|
+
private _native;
|
|
65
|
+
constructor(address: string | URL | null, protocols?: string | string[], options?: ClientOptions);
|
|
66
|
+
/** Merge `protocols` arg and `options.protocols` / `options.protocol`. */
|
|
67
|
+
private _resolveProtocols;
|
|
68
|
+
/** Lazy-open the underlying native WebSocket. Separated from the constructor
|
|
69
|
+
* so subclasses or future socket-adoption paths can bypass it. */
|
|
70
|
+
private _openNative;
|
|
71
|
+
private _fail;
|
|
72
|
+
private _onOpen;
|
|
73
|
+
private _onMessage;
|
|
74
|
+
/** Convert an ArrayBuffer to the Buffer flavor requested by `binaryType`. */
|
|
75
|
+
private _decodeBinary;
|
|
76
|
+
private _onClose;
|
|
77
|
+
private _onError;
|
|
78
|
+
send(data: string | Buffer | ArrayBuffer | ArrayBufferView | Blob | number | boolean, optionsOrCb?: ((err?: Error) => void) | {
|
|
79
|
+
mask?: boolean;
|
|
80
|
+
binary?: boolean;
|
|
81
|
+
compress?: boolean;
|
|
82
|
+
fin?: boolean;
|
|
83
|
+
}, cb?: (err?: Error) => void): void;
|
|
84
|
+
private _nativeSend;
|
|
85
|
+
private _sizeOf;
|
|
86
|
+
close(code?: number, reason?: string | Buffer): void;
|
|
87
|
+
/** ws-only: force-close without sending a Close frame. On Gjs we can't bypass
|
|
88
|
+
* Soup's close handshake, so terminate is approximated as close(1006).
|
|
89
|
+
* Known gap vs. ws semantics — documented. */
|
|
90
|
+
terminate(): void;
|
|
91
|
+
/** Convenience: returns true if the socket is closed or closing. Matches
|
|
92
|
+
* ws.isPaused() / internal state checks that some consumers rely on. */
|
|
93
|
+
get isPaused(): boolean;
|
|
94
|
+
private _eventTargetListeners;
|
|
95
|
+
addEventListener(type: string, listener: (ev: any) => void): void;
|
|
96
|
+
removeEventListener(type: string, listener: (ev: any) => void): void;
|
|
97
|
+
private _dispatchEvent;
|
|
98
|
+
static get BINARY_TYPES(): readonly ["nodebuffer", "arraybuffer", "fragments"];
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/ws",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"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.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.2.0",
|
|
38
|
+
"@gjsify/crypto": "^0.2.0",
|
|
39
|
+
"@gjsify/events": "^0.2.0",
|
|
40
|
+
"@gjsify/utils": "^0.2.0",
|
|
41
|
+
"@gjsify/websocket": "^0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@gjsify/cli": "^0.2.0",
|
|
45
|
+
"@gjsify/unit": "^0.2.0",
|
|
46
|
+
"@types/node": "^25.6.0",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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');
|