@nx.js/ws 0.0.1

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # `@nx.js/ws`
2
+
3
+ WebSocket server for [nx.js](https://nxjs.n8.io).
4
+
5
+ Provides a `WebSocketServer` class inspired by the [`ws`](https://github.com/websockets/ws) npm package, but using the Web `EventTarget` API instead of Node.js `EventEmitter`.
6
+
7
+ ## Usage
8
+
9
+ ```typescript
10
+ import { WebSocketServer } from '@nx.js/ws';
11
+
12
+ const wss = new WebSocketServer({ port: 8080 });
13
+
14
+ wss.addEventListener('listening', () => {
15
+ console.log('WebSocket server is listening on port 8080');
16
+ });
17
+
18
+ wss.addEventListener('connection', (e) => {
19
+ const { socket, request } = e.detail;
20
+ console.log('Client connected');
21
+
22
+ socket.addEventListener('message', (ev) => {
23
+ console.log('Received:', ev.data);
24
+ socket.send(`Echo: ${ev.data}`);
25
+ });
26
+
27
+ socket.addEventListener('close', (ev) => {
28
+ console.log('Client disconnected', ev.code, ev.reason);
29
+ });
30
+ });
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### `WebSocketServer`
36
+
37
+ - **Constructor:** `new WebSocketServer({ port, host? })`
38
+ - **Events:** `connection`, `listening`, `close`, `error`
39
+ - **Properties:** `clients` (Set of connected `ServerWebSocket` instances)
40
+ - **Methods:** `close()`, `address()`
41
+
42
+ ### `ServerWebSocket`
43
+
44
+ - **Events:** `open`, `message`, `close`, `error`
45
+ - **Properties:** `readyState`, `bufferedAmount`, `binaryType`, `url`, `protocol`, `extensions`
46
+ - **Methods:** `send(data)`, `close(code?, reason?)`
47
+
48
+ ## Protocol Compliance
49
+
50
+ - Implements RFC 6455 WebSocket protocol
51
+ - Server frames are NOT masked (per spec)
52
+ - Client frames MUST be masked (validated; connection closed with 1002 if not)
53
+ - Handles ping/pong automatically
54
+ - Supports fragmented messages
55
+ - Supports text and binary frames
@@ -0,0 +1,131 @@
1
+ /**
2
+ * WebSocket protocol constants and frame utilities per RFC 6455.
3
+ *
4
+ * Shared between the client ({@link WebSocket}) and
5
+ * server ({@link WebSocketServer}) implementations.
6
+ *
7
+ * @module
8
+ */
9
+ /** Continuation frame. */
10
+ export declare const OP_CONTINUATION = 0;
11
+ /** Text data frame. */
12
+ export declare const OP_TEXT = 1;
13
+ /** Binary data frame. */
14
+ export declare const OP_BINARY = 2;
15
+ /** Connection close frame. */
16
+ export declare const OP_CLOSE = 8;
17
+ /** Ping frame. */
18
+ export declare const OP_PING = 9;
19
+ /** Pong frame. */
20
+ export declare const OP_PONG = 10;
21
+ /** The connection has not yet been established. */
22
+ export declare const CONNECTING = 0;
23
+ /** The connection is established and communication is possible. */
24
+ export declare const OPEN = 1;
25
+ /** The connection is going through the closing handshake. */
26
+ export declare const CLOSING = 2;
27
+ /** The connection has been closed or could not be opened. */
28
+ export declare const CLOSED = 3;
29
+ /**
30
+ * Parsed WebSocket frame header.
31
+ */
32
+ export interface FrameHeader {
33
+ /** Whether this is the final fragment. */
34
+ fin: boolean;
35
+ /** The frame opcode. */
36
+ opcode: number;
37
+ /** Whether the payload is masked. */
38
+ masked: boolean;
39
+ /** The length of the payload in bytes. */
40
+ payloadLength: number;
41
+ /** The 4-byte mask key, or `null` if not masked. */
42
+ maskKey: Uint8Array | null;
43
+ /** The total size of the header in bytes (including mask key). */
44
+ headerSize: number;
45
+ }
46
+ /**
47
+ * Parse a WebSocket frame header from raw bytes.
48
+ *
49
+ * Returns `null` if the buffer does not contain enough data
50
+ * for a complete header.
51
+ */
52
+ export declare function parseFrameHeader(data: Uint8Array): FrameHeader | null;
53
+ /**
54
+ * XOR-mask (or unmask) a payload with a 4-byte mask key.
55
+ */
56
+ export declare function maskData(data: Uint8Array, maskKey: Uint8Array): Uint8Array;
57
+ /**
58
+ * Build a WebSocket frame.
59
+ *
60
+ * @param opcode - The frame opcode.
61
+ * @param payload - The payload data.
62
+ * @param mask - Whether to mask the payload (required for client-to-server frames).
63
+ */
64
+ export declare function buildFrame(opcode: number, payload: Uint8Array, mask: boolean): Uint8Array;
65
+ /**
66
+ * Encode a close frame payload from a code and optional reason.
67
+ */
68
+ export declare function encodeClosePayload(code?: number, reason?: string): Uint8Array;
69
+ /**
70
+ * Decode a close frame payload into a code and reason string.
71
+ */
72
+ export declare function decodeClosePayload(payload: Uint8Array): {
73
+ code: number;
74
+ reason: string;
75
+ };
76
+ /**
77
+ * Concatenate two `Uint8Array`s.
78
+ */
79
+ export declare function concat(a: Uint8Array, b: Uint8Array): Uint8Array;
80
+ /**
81
+ * The WebSocket magic GUID used in the opening handshake (RFC 6455 Section 4.2.2).
82
+ */
83
+ export declare const WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
84
+ /**
85
+ * Compute the `Sec-WebSocket-Accept` header value for a given key.
86
+ *
87
+ * Used by the server to generate the response, and by the client to validate it.
88
+ */
89
+ export declare function computeAcceptKey(key: string): Promise<string>;
90
+ /**
91
+ * Well-known symbol used to signal the internal constructor path.
92
+ * Using `Symbol.for` ensures it's the same symbol across all bundles
93
+ * (the runtime bundle and user app bundles share the global symbol registry).
94
+ */
95
+ export declare const INTERNAL_SYMBOL: unique symbol;
96
+ /**
97
+ * Initialization options for creating a server-side WebSocket.
98
+ */
99
+ export interface WebSocketInit {
100
+ /** The writable stream writer for sending frames. */
101
+ writer: WritableStreamDefaultWriter<Uint8Array>;
102
+ /** The readable stream reader for receiving frames. */
103
+ reader: ReadableStreamDefaultReader<Uint8Array>;
104
+ /** The URL of the WebSocket connection. */
105
+ url: string;
106
+ /** The negotiated sub-protocol. */
107
+ protocol: string;
108
+ /** The negotiated extensions. */
109
+ extensions: string;
110
+ /** Any leftover bytes from the HTTP upgrade that belong to the WebSocket stream. */
111
+ initialBuffer: Uint8Array;
112
+ /** Whether to mask outgoing frames (true for client, false for server). */
113
+ mask: boolean;
114
+ /** Whether to require incoming frames to be masked (true for server). */
115
+ requireMask: boolean;
116
+ /** Called when the WebSocket is cleaned up (e.g. to close the underlying TCP socket). */
117
+ onCleanup?: () => void;
118
+ }
119
+ /**
120
+ * Create a pre-connected WebSocket instance for server-side use.
121
+ *
122
+ * The returned instance is a real `WebSocket` object (passes `instanceof`)
123
+ * with its read loop already started. Server frames are NOT masked.
124
+ *
125
+ * Uses `globalThis.WebSocket` with the well-known `INTERNAL_SYMBOL` to
126
+ * trigger the internal constructor path, avoiding any cross-bundle
127
+ * registration issues.
128
+ *
129
+ * @internal
130
+ */
131
+ export declare function createWebSocket(init: WebSocketInit): WebSocket;
package/dist/frame.js ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * WebSocket protocol constants and frame utilities per RFC 6455.
3
+ *
4
+ * Shared between the client ({@link WebSocket}) and
5
+ * server ({@link WebSocketServer}) implementations.
6
+ *
7
+ * @module
8
+ */
9
+ const encoder = new TextEncoder();
10
+ const decoder = new TextDecoder();
11
+ // ---- Opcodes ----
12
+ /** Continuation frame. */
13
+ export const OP_CONTINUATION = 0x0;
14
+ /** Text data frame. */
15
+ export const OP_TEXT = 0x1;
16
+ /** Binary data frame. */
17
+ export const OP_BINARY = 0x2;
18
+ /** Connection close frame. */
19
+ export const OP_CLOSE = 0x8;
20
+ /** Ping frame. */
21
+ export const OP_PING = 0x9;
22
+ /** Pong frame. */
23
+ export const OP_PONG = 0xa;
24
+ // ---- Ready states ----
25
+ /** The connection has not yet been established. */
26
+ export const CONNECTING = 0;
27
+ /** The connection is established and communication is possible. */
28
+ export const OPEN = 1;
29
+ /** The connection is going through the closing handshake. */
30
+ export const CLOSING = 2;
31
+ /** The connection has been closed or could not be opened. */
32
+ export const CLOSED = 3;
33
+ /**
34
+ * Parse a WebSocket frame header from raw bytes.
35
+ *
36
+ * Returns `null` if the buffer does not contain enough data
37
+ * for a complete header.
38
+ */
39
+ export function parseFrameHeader(data) {
40
+ if (data.length < 2)
41
+ return null;
42
+ const fin = !!(data[0] & 0x80);
43
+ const opcode = data[0] & 0x0f;
44
+ const masked = !!(data[1] & 0x80);
45
+ let payloadLength = data[1] & 0x7f;
46
+ let offset = 2;
47
+ if (payloadLength === 126) {
48
+ if (data.length < 4)
49
+ return null;
50
+ payloadLength = (data[2] << 8) | data[3];
51
+ offset = 4;
52
+ }
53
+ else if (payloadLength === 127) {
54
+ if (data.length < 10)
55
+ return null;
56
+ const view = new DataView(data.buffer, data.byteOffset);
57
+ payloadLength = Number(view.getBigUint64(2));
58
+ offset = 10;
59
+ }
60
+ let maskKey = null;
61
+ if (masked) {
62
+ if (data.length < offset + 4)
63
+ return null;
64
+ maskKey = data.slice(offset, offset + 4);
65
+ offset += 4;
66
+ }
67
+ return { fin, opcode, masked, payloadLength, maskKey, headerSize: offset };
68
+ }
69
+ // ---- Frame building ----
70
+ /**
71
+ * XOR-mask (or unmask) a payload with a 4-byte mask key.
72
+ */
73
+ export function maskData(data, maskKey) {
74
+ const result = new Uint8Array(data.length);
75
+ for (let i = 0; i < data.length; i++) {
76
+ result[i] = data[i] ^ maskKey[i & 3];
77
+ }
78
+ return result;
79
+ }
80
+ /**
81
+ * Build a WebSocket frame.
82
+ *
83
+ * @param opcode - The frame opcode.
84
+ * @param payload - The payload data.
85
+ * @param mask - Whether to mask the payload (required for client-to-server frames).
86
+ */
87
+ export function buildFrame(opcode, payload, mask) {
88
+ let headerLen = 2;
89
+ if (payload.length > 65535)
90
+ headerLen += 8;
91
+ else if (payload.length > 125)
92
+ headerLen += 2;
93
+ const maskKeyLen = mask ? 4 : 0;
94
+ const frame = new Uint8Array(headerLen + maskKeyLen + payload.length);
95
+ frame[0] = 0x80 | opcode; // FIN + opcode
96
+ let offset = 1;
97
+ const maskBit = mask ? 0x80 : 0;
98
+ if (payload.length > 65535) {
99
+ frame[offset++] = maskBit | 127;
100
+ const view = new DataView(frame.buffer);
101
+ view.setBigUint64(offset, BigInt(payload.length));
102
+ offset += 8;
103
+ }
104
+ else if (payload.length > 125) {
105
+ frame[offset++] = maskBit | 126;
106
+ frame[offset++] = (payload.length >> 8) & 0xff;
107
+ frame[offset++] = payload.length & 0xff;
108
+ }
109
+ else {
110
+ frame[offset++] = maskBit | payload.length;
111
+ }
112
+ if (mask) {
113
+ const maskKey = new Uint8Array(4);
114
+ crypto.getRandomValues(maskKey);
115
+ frame.set(maskKey, offset);
116
+ offset += 4;
117
+ const masked = maskData(payload, maskKey);
118
+ frame.set(masked, offset);
119
+ }
120
+ else {
121
+ frame.set(payload, offset);
122
+ }
123
+ return frame;
124
+ }
125
+ // ---- Close frame helpers ----
126
+ /**
127
+ * Encode a close frame payload from a code and optional reason.
128
+ */
129
+ export function encodeClosePayload(code, reason) {
130
+ if (code === undefined)
131
+ return new Uint8Array(0);
132
+ const reasonBytes = reason ? encoder.encode(reason) : new Uint8Array(0);
133
+ const payload = new Uint8Array(2 + reasonBytes.length);
134
+ payload[0] = (code >> 8) & 0xff;
135
+ payload[1] = code & 0xff;
136
+ payload.set(reasonBytes, 2);
137
+ return payload;
138
+ }
139
+ /**
140
+ * Decode a close frame payload into a code and reason string.
141
+ */
142
+ export function decodeClosePayload(payload) {
143
+ if (payload.length < 2)
144
+ return { code: 1005, reason: '' };
145
+ const code = (payload[0] << 8) | payload[1];
146
+ const reason = payload.length > 2 ? decoder.decode(payload.slice(2)) : '';
147
+ return { code, reason };
148
+ }
149
+ // ---- Utilities ----
150
+ /**
151
+ * Concatenate two `Uint8Array`s.
152
+ */
153
+ export function concat(a, b) {
154
+ const c = new Uint8Array(a.length + b.length);
155
+ c.set(a, 0);
156
+ c.set(b, a.length);
157
+ return c;
158
+ }
159
+ /**
160
+ * The WebSocket magic GUID used in the opening handshake (RFC 6455 Section 4.2.2).
161
+ */
162
+ export const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
163
+ /**
164
+ * Compute the `Sec-WebSocket-Accept` header value for a given key.
165
+ *
166
+ * Used by the server to generate the response, and by the client to validate it.
167
+ */
168
+ export async function computeAcceptKey(key) {
169
+ const data = encoder.encode(key + WS_MAGIC);
170
+ const hash = await crypto.subtle.digest('SHA-1', data);
171
+ return new Uint8Array(hash).toBase64();
172
+ }
173
+ // ---- Server-side WebSocket creation ----
174
+ /**
175
+ * Well-known symbol used to signal the internal constructor path.
176
+ * Using `Symbol.for` ensures it's the same symbol across all bundles
177
+ * (the runtime bundle and user app bundles share the global symbol registry).
178
+ */
179
+ export const INTERNAL_SYMBOL = Symbol.for('nx.ws.internal');
180
+ /**
181
+ * Create a pre-connected WebSocket instance for server-side use.
182
+ *
183
+ * The returned instance is a real `WebSocket` object (passes `instanceof`)
184
+ * with its read loop already started. Server frames are NOT masked.
185
+ *
186
+ * Uses `globalThis.WebSocket` with the well-known `INTERNAL_SYMBOL` to
187
+ * trigger the internal constructor path, avoiding any cross-bundle
188
+ * registration issues.
189
+ *
190
+ * @internal
191
+ */
192
+ export function createWebSocket(init) {
193
+ return new globalThis.WebSocket(INTERNAL_SYMBOL, init);
194
+ }
195
+ //# sourceMappingURL=frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.js","sourceRoot":"","sources":["../src/frame.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export type { ConnectionEventDetail, WebSocketServerOptions } from './server';
2
+ export { WebSocketServer } from './server';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { WebSocketServer } from './server';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,54 @@
1
+ export interface WebSocketServerOptions {
2
+ /** Port to listen on. */
3
+ port: number;
4
+ /** IP address to bind to (defaults to `0.0.0.0`). */
5
+ host?: string;
6
+ }
7
+ export interface ConnectionEventDetail {
8
+ /** The connected WebSocket. */
9
+ socket: WebSocket;
10
+ /** The original HTTP upgrade request. */
11
+ request: Request;
12
+ }
13
+ /**
14
+ * A WebSocket server that listens for incoming connections on a TCP port.
15
+ *
16
+ * Uses the Web `EventTarget` API and dispatches `CustomEvent` instances.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { WebSocketServer } from '@nx.js/ws';
21
+ *
22
+ * const wss = new WebSocketServer({ port: 8080 });
23
+ *
24
+ * wss.addEventListener('connection', (e) => {
25
+ * const { socket, request } = e.detail;
26
+ * console.log('Client connected from', request.url);
27
+ *
28
+ * socket.addEventListener('message', (ev) => {
29
+ * console.log('Received:', ev.data);
30
+ * socket.send(`Echo: ${ev.data}`);
31
+ * });
32
+ *
33
+ * socket.addEventListener('close', () => {
34
+ * console.log('Client disconnected');
35
+ * });
36
+ * });
37
+ * ```
38
+ */
39
+ export declare class WebSocketServer extends EventTarget {
40
+ #private;
41
+ readonly clients: Set<WebSocket>;
42
+ constructor(opts: WebSocketServerOptions);
43
+ /**
44
+ * Returns the address the server is listening on.
45
+ */
46
+ address(): {
47
+ port: number;
48
+ host: string;
49
+ } | null;
50
+ /**
51
+ * Close the server and all connected clients.
52
+ */
53
+ close(): void;
54
+ }
package/dist/server.js ADDED
@@ -0,0 +1,157 @@
1
+ import { computeAcceptKey, concat, createWebSocket } from './frame';
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
4
+ /**
5
+ * A WebSocket server that listens for incoming connections on a TCP port.
6
+ *
7
+ * Uses the Web `EventTarget` API and dispatches `CustomEvent` instances.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { WebSocketServer } from '@nx.js/ws';
12
+ *
13
+ * const wss = new WebSocketServer({ port: 8080 });
14
+ *
15
+ * wss.addEventListener('connection', (e) => {
16
+ * const { socket, request } = e.detail;
17
+ * console.log('Client connected from', request.url);
18
+ *
19
+ * socket.addEventListener('message', (ev) => {
20
+ * console.log('Received:', ev.data);
21
+ * socket.send(`Echo: ${ev.data}`);
22
+ * });
23
+ *
24
+ * socket.addEventListener('close', () => {
25
+ * console.log('Client disconnected');
26
+ * });
27
+ * });
28
+ * ```
29
+ */
30
+ export class WebSocketServer extends EventTarget {
31
+ clients = new Set();
32
+ #server;
33
+ constructor(opts) {
34
+ super();
35
+ const { port, host } = opts;
36
+ this.#server = Switch.listen({
37
+ port,
38
+ ip: host,
39
+ accept: (event) => {
40
+ this.#handleConnection(event.socket);
41
+ },
42
+ });
43
+ // Dispatch listening event on next microtask
44
+ queueMicrotask(() => {
45
+ this.dispatchEvent(new Event('listening'));
46
+ });
47
+ }
48
+ /**
49
+ * Returns the address the server is listening on.
50
+ */
51
+ address() {
52
+ return null;
53
+ }
54
+ /**
55
+ * Close the server and all connected clients.
56
+ */
57
+ close() {
58
+ for (const client of this.clients) {
59
+ client.close(1001, 'Server shutting down');
60
+ }
61
+ this.clients.clear();
62
+ this.#server.close();
63
+ this.dispatchEvent(new Event('close'));
64
+ }
65
+ async #handleConnection(socket) {
66
+ try {
67
+ const reader = socket.readable.getReader();
68
+ let buffer = new Uint8Array(0);
69
+ // Read headers until we find \r\n\r\n
70
+ while (true) {
71
+ const { done, value } = await reader.read();
72
+ if (done)
73
+ return;
74
+ buffer = concat(buffer, value);
75
+ const headerEnd = findHeaderEnd(buffer);
76
+ if (headerEnd !== -1) {
77
+ const headerBytes = buffer.slice(0, headerEnd);
78
+ const remaining = buffer.slice(headerEnd + 4);
79
+ reader.releaseLock();
80
+ const headerStr = decoder.decode(headerBytes);
81
+ const lines = headerStr.split('\r\n');
82
+ const [method, path] = lines[0].split(' ');
83
+ const headers = new Headers();
84
+ for (let i = 1; i < lines.length; i++) {
85
+ const col = lines[i].indexOf(':');
86
+ if (col !== -1) {
87
+ headers.set(lines[i].slice(0, col), lines[i].slice(col + 1).trim());
88
+ }
89
+ }
90
+ const host = headers.get('host') || 'localhost';
91
+ const request = new Request(`http://${host}${path}`, {
92
+ method,
93
+ headers,
94
+ });
95
+ // Validate WebSocket upgrade
96
+ const upgrade = headers.get('upgrade');
97
+ const key = headers.get('sec-websocket-key');
98
+ if (!upgrade || upgrade.toLowerCase() !== 'websocket' || !key) {
99
+ const writer = socket.writable.getWriter();
100
+ await writer.write(encoder.encode('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'));
101
+ await writer.close();
102
+ return;
103
+ }
104
+ // Compute accept key
105
+ const acceptKey = await computeAcceptKey(key);
106
+ // Send 101 Switching Protocols
107
+ const writer = socket.writable.getWriter();
108
+ const responseHeaders = [
109
+ 'HTTP/1.1 101 Switching Protocols',
110
+ 'Upgrade: websocket',
111
+ 'Connection: Upgrade',
112
+ `Sec-WebSocket-Accept: ${acceptKey}`,
113
+ '',
114
+ '',
115
+ ].join('\r\n');
116
+ await writer.write(encoder.encode(responseHeaders));
117
+ writer.releaseLock();
118
+ // Create a real WebSocket instance via the factory
119
+ const ws = createWebSocket({
120
+ writer: socket.writable.getWriter(),
121
+ reader: socket.readable.getReader(),
122
+ url: `ws://${host}${path}`,
123
+ protocol: '',
124
+ extensions: '',
125
+ initialBuffer: remaining,
126
+ mask: false,
127
+ requireMask: true,
128
+ onCleanup: () => socket.close(),
129
+ });
130
+ this.clients.add(ws);
131
+ ws.addEventListener('close', () => {
132
+ this.clients.delete(ws);
133
+ });
134
+ this.dispatchEvent(new CustomEvent('connection', {
135
+ detail: { socket: ws, request },
136
+ }));
137
+ return;
138
+ }
139
+ }
140
+ }
141
+ catch (err) {
142
+ this.dispatchEvent(new CustomEvent('error', { detail: err }));
143
+ }
144
+ }
145
+ }
146
+ function findHeaderEnd(data) {
147
+ for (let i = 0; i < data.length - 3; i++) {
148
+ if (data[i] === 13 &&
149
+ data[i + 1] === 10 &&
150
+ data[i + 2] === 13 &&
151
+ data[i + 3] === 10) {
152
+ return i;
153
+ }
154
+ }
155
+ return -1;
156
+ }
157
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ export interface WebSocketEventMap {
2
+ open: Event;
3
+ message: MessageEvent;
4
+ close: CloseEvent;
5
+ error: Event;
6
+ }
7
+ /**
8
+ * Server-side WebSocket connection.
9
+ *
10
+ * Uses the Web `EventTarget` API (not Node.js `EventEmitter`).
11
+ * Server frames are NOT masked per RFC 6455.
12
+ */
13
+ export declare class ServerWebSocket extends EventTarget {
14
+ #private;
15
+ static readonly CONNECTING = 0;
16
+ static readonly OPEN = 1;
17
+ static readonly CLOSING = 2;
18
+ static readonly CLOSED = 3;
19
+ readonly CONNECTING = 0;
20
+ readonly OPEN = 1;
21
+ readonly CLOSING = 2;
22
+ readonly CLOSED = 3;
23
+ /** The URL of the connection (from the HTTP upgrade request). */
24
+ readonly url: string;
25
+ /** The protocol negotiated during the handshake. */
26
+ readonly protocol: string;
27
+ /** The extensions negotiated during the handshake. */
28
+ readonly extensions: string;
29
+ constructor(writer: WritableStreamDefaultWriter<Uint8Array>, url: string, protocol?: string, extensions?: string);
30
+ get readyState(): number;
31
+ get bufferedAmount(): number;
32
+ get binaryType(): BinaryType;
33
+ set binaryType(value: BinaryType);
34
+ /** @internal Mark the socket as open and dispatch the `open` event. */
35
+ _open(): void;
36
+ /**
37
+ * Send data over the WebSocket connection.
38
+ */
39
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
40
+ /**
41
+ * Initiate the closing handshake.
42
+ */
43
+ close(code?: number, reason?: string): void;
44
+ /** @internal Process an incoming frame. */
45
+ _handleFrame(opcode: number, fin: boolean, payload: Uint8Array): void;
46
+ }
@@ -0,0 +1,167 @@
1
+ import { buildFrame, CLOSED, CLOSING, CONNECTING, decodeClosePayload, encodeClosePayload, OP_BINARY, OP_CLOSE, OP_CONTINUATION, OP_PING, OP_PONG, OP_TEXT, OPEN, } from './frame';
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
4
+ /**
5
+ * Server-side WebSocket connection.
6
+ *
7
+ * Uses the Web `EventTarget` API (not Node.js `EventEmitter`).
8
+ * Server frames are NOT masked per RFC 6455.
9
+ */
10
+ export class ServerWebSocket extends EventTarget {
11
+ static CONNECTING = CONNECTING;
12
+ static OPEN = OPEN;
13
+ static CLOSING = CLOSING;
14
+ static CLOSED = CLOSED;
15
+ CONNECTING = CONNECTING;
16
+ OPEN = OPEN;
17
+ CLOSING = CLOSING;
18
+ CLOSED = CLOSED;
19
+ #readyState = CONNECTING;
20
+ #writer;
21
+ #binaryType = 'blob';
22
+ #bufferedAmount = 0;
23
+ #fragments = [];
24
+ #fragmentOpcode = null;
25
+ /** The URL of the connection (from the HTTP upgrade request). */
26
+ url;
27
+ /** The protocol negotiated during the handshake. */
28
+ protocol;
29
+ /** The extensions negotiated during the handshake. */
30
+ extensions;
31
+ constructor(writer, url, protocol = '', extensions = '') {
32
+ super();
33
+ this.#writer = writer;
34
+ this.url = url;
35
+ this.protocol = protocol;
36
+ this.extensions = extensions;
37
+ }
38
+ get readyState() {
39
+ return this.#readyState;
40
+ }
41
+ get bufferedAmount() {
42
+ return this.#bufferedAmount;
43
+ }
44
+ get binaryType() {
45
+ return this.#binaryType;
46
+ }
47
+ set binaryType(value) {
48
+ if (value !== 'blob' && value !== 'arraybuffer') {
49
+ throw new SyntaxError(`Invalid binaryType: ${value}`);
50
+ }
51
+ this.#binaryType = value;
52
+ }
53
+ /** @internal Mark the socket as open and dispatch the `open` event. */
54
+ _open() {
55
+ this.#readyState = OPEN;
56
+ this.dispatchEvent(new Event('open'));
57
+ }
58
+ /**
59
+ * Send data over the WebSocket connection.
60
+ */
61
+ send(data) {
62
+ if (this.#readyState !== OPEN) {
63
+ throw new DOMException('WebSocket is not open', 'InvalidStateError');
64
+ }
65
+ let payload;
66
+ let opcode;
67
+ if (typeof data === 'string') {
68
+ payload = encoder.encode(data);
69
+ opcode = OP_TEXT;
70
+ }
71
+ else if (data instanceof ArrayBuffer ||
72
+ data instanceof SharedArrayBuffer) {
73
+ payload = new Uint8Array(data);
74
+ opcode = OP_BINARY;
75
+ }
76
+ else if (ArrayBuffer.isView(data)) {
77
+ payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
78
+ opcode = OP_BINARY;
79
+ }
80
+ else {
81
+ throw new TypeError('Blob send is not supported; convert to ArrayBuffer first');
82
+ }
83
+ // Server frames are NOT masked
84
+ const frame = buildFrame(opcode, payload, false);
85
+ this.#bufferedAmount += frame.length;
86
+ this.#writer.write(frame).then(() => {
87
+ this.#bufferedAmount -= frame.length;
88
+ });
89
+ }
90
+ /**
91
+ * Initiate the closing handshake.
92
+ */
93
+ close(code, reason) {
94
+ if (this.#readyState === CLOSING || this.#readyState === CLOSED) {
95
+ return;
96
+ }
97
+ this.#readyState = CLOSING;
98
+ const payload = encodeClosePayload(code, reason);
99
+ const frame = buildFrame(OP_CLOSE, payload, false);
100
+ this.#writer.write(frame);
101
+ }
102
+ /** @internal Process an incoming frame. */
103
+ _handleFrame(opcode, fin, payload) {
104
+ switch (opcode) {
105
+ case OP_TEXT:
106
+ case OP_BINARY: {
107
+ if (!fin) {
108
+ this.#fragmentOpcode = opcode;
109
+ this.#fragments.push(payload);
110
+ return;
111
+ }
112
+ this.#dispatchMessage(opcode, payload);
113
+ break;
114
+ }
115
+ case OP_CONTINUATION: {
116
+ this.#fragments.push(payload);
117
+ if (fin) {
118
+ const totalLen = this.#fragments.reduce((s, f) => s + f.length, 0);
119
+ const assembled = new Uint8Array(totalLen);
120
+ let offset = 0;
121
+ for (const frag of this.#fragments) {
122
+ assembled.set(frag, offset);
123
+ offset += frag.length;
124
+ }
125
+ const msgOpcode = this.#fragmentOpcode;
126
+ this.#fragments = [];
127
+ this.#fragmentOpcode = null;
128
+ this.#dispatchMessage(msgOpcode, assembled);
129
+ }
130
+ break;
131
+ }
132
+ case OP_PING: {
133
+ const pong = buildFrame(OP_PONG, payload, false);
134
+ this.#writer.write(pong);
135
+ break;
136
+ }
137
+ case OP_PONG:
138
+ break;
139
+ case OP_CLOSE: {
140
+ const { code, reason } = decodeClosePayload(payload);
141
+ if (this.#readyState === OPEN) {
142
+ const closeFrame = buildFrame(OP_CLOSE, payload, false);
143
+ this.#writer.write(closeFrame);
144
+ }
145
+ this.#readyState = CLOSED;
146
+ this.#writer.close().catch(() => { });
147
+ this.dispatchEvent(new CloseEvent('close', {
148
+ code,
149
+ reason,
150
+ wasClean: true,
151
+ }));
152
+ break;
153
+ }
154
+ }
155
+ }
156
+ #dispatchMessage(opcode, payload) {
157
+ let data;
158
+ if (opcode === OP_TEXT) {
159
+ data = decoder.decode(payload);
160
+ }
161
+ else {
162
+ data = payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength);
163
+ }
164
+ this.dispatchEvent(new MessageEvent('message', { data }));
165
+ }
166
+ }
167
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@nx.js/ws",
3
+ "version": "0.0.1",
4
+ "description": "WebSocket server for nx.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/TooTallNate/nx.js.git",
8
+ "directory": "packages/ws"
9
+ },
10
+ "homepage": "https://nxjs.n8.io/ws",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./frame": {
17
+ "types": "./dist/frame.d.ts",
18
+ "default": "./dist/frame.js"
19
+ }
20
+ },
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "typesVersions": {
24
+ "*": {
25
+ "frame": [
26
+ "dist/frame.d.ts"
27
+ ]
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "docs": "typedoc && ../../type-aliases-meta.sh"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "keywords": [
41
+ "nx.js",
42
+ "switch",
43
+ "websocket",
44
+ "server",
45
+ "ws"
46
+ ],
47
+ "author": "Nathan Rajlich <n@n8.io>",
48
+ "license": "MIT"
49
+ }