@forklaunch/ws 0.1.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/index.mjs ADDED
@@ -0,0 +1,491 @@
1
+ // src/webSocket.ts
2
+ import {
3
+ createWebSocketSchemas,
4
+ decodeSchemaValue,
5
+ encodeSchemaValue,
6
+ normalizeEncodedValue
7
+ } from "@forklaunch/core/ws";
8
+ import { WebSocket } from "ws";
9
+ var ForklaunchWebSocket = class extends WebSocket {
10
+ eventSchemas;
11
+ schemas;
12
+ schemaValidator;
13
+ /**
14
+ * Creates a new ForklaunchWebSocket instance with schema validation.
15
+ *
16
+ * @param schemaValidator - The schema validator instance (e.g., ZodSchemaValidator)
17
+ * @param eventSchemas - Schema definitions for all WebSocket events
18
+ * @param websocketParams - Standard WebSocket constructor parameters (address, protocols, options)
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const ws = new ForklaunchWebSocket(
23
+ * validator,
24
+ * schemas,
25
+ * 'ws://localhost:8080',
26
+ * ['chat-protocol'],
27
+ * { handshakeTimeout: 5000 }
28
+ * );
29
+ * ```
30
+ */
31
+ constructor(schemaValidator, eventSchemas, ...websocketParams) {
32
+ super(...websocketParams);
33
+ this.schemaValidator = schemaValidator;
34
+ this.eventSchemas = eventSchemas;
35
+ this.schemas = createWebSocketSchemas(
36
+ schemaValidator,
37
+ eventSchemas
38
+ );
39
+ }
40
+ /**
41
+ * Decodes and validates incoming data from the WebSocket.
42
+ *
43
+ * This method handles multiple data formats:
44
+ * - Buffer → decoded to UTF-8 string → parsed as JSON → validated
45
+ * - ArrayBuffer → converted to Buffer → decoded → parsed → validated
46
+ * - TypedArray → converted to Buffer → decoded → parsed → validated
47
+ * - String → parsed as JSON → validated
48
+ * - Object → validated directly (already parsed)
49
+ *
50
+ * @param data - The raw data received from the WebSocket
51
+ * @param schema - Optional schema to validate against
52
+ * @returns The validated and decoded data
53
+ * @throws {Error} If validation fails with pretty-printed error messages
54
+ *
55
+ * @internal
56
+ */
57
+ decodeAndValidate(data, schema) {
58
+ return decodeSchemaValue(
59
+ this.schemaValidator,
60
+ data,
61
+ schema,
62
+ "web socket event"
63
+ );
64
+ }
65
+ /**
66
+ * Validates and encodes outgoing data for transmission over the WebSocket.
67
+ *
68
+ * This method:
69
+ * 1. Validates data against the provided schema (if present)
70
+ * 2. Encodes data to Buffer format:
71
+ * - Buffer → returned as-is
72
+ * - Object/Array → JSON.stringify → Buffer
73
+ * - String → Buffer
74
+ * - Number/Boolean → JSON.stringify → Buffer
75
+ * - null/undefined → returned as-is
76
+ *
77
+ * @param data - The data to validate and encode
78
+ * @param schema - Optional schema to validate against
79
+ * @returns The validated and encoded data as Buffer, or null/undefined
80
+ * @throws {Error} If validation fails with pretty-printed error messages
81
+ *
82
+ * @internal
83
+ */
84
+ validateAndEncode(data, schema, allowUndefined = false, context = "web socket event") {
85
+ const encoded = encodeSchemaValue(
86
+ this.schemaValidator,
87
+ data,
88
+ schema,
89
+ context
90
+ );
91
+ return normalizeEncodedValue(encoded, context, allowUndefined);
92
+ }
93
+ /**
94
+ * Transforms incoming event arguments by decoding and validating them.
95
+ *
96
+ * Applies transformation for:
97
+ * - `message` events: validates message data
98
+ * - `close` events: validates close reason
99
+ * - `ping` events: validates ping data
100
+ * - `pong` events: validates pong data
101
+ *
102
+ * @param event - The event name
103
+ * @param args - The raw event arguments
104
+ * @returns Transformed and validated arguments
105
+ *
106
+ * @internal
107
+ */
108
+ transformIncomingArgs(event, args) {
109
+ let transformedArgs = args;
110
+ if (event === "message" && args.length >= 2) {
111
+ const data = args[0];
112
+ const isBinary = args[1];
113
+ const serverSchema = this.schemas.serverMessagesSchema;
114
+ if (typeof isBinary === "boolean" && isBinary && serverSchema) {
115
+ const validated = this.decodeAndValidate(data, serverSchema);
116
+ transformedArgs = [validated, false, ...args.slice(2)];
117
+ }
118
+ } else if (event === "close" && args.length >= 2) {
119
+ const code = args[0];
120
+ const reason = args[1];
121
+ const validated = this.decodeAndValidate(
122
+ reason,
123
+ this.schemas.closeReasonSchema
124
+ );
125
+ transformedArgs = [code, validated, ...args.slice(2)];
126
+ } else if (event === "ping" && args.length >= 1) {
127
+ const data = args[0];
128
+ const validated = this.decodeAndValidate(data, this.schemas.pingSchema);
129
+ transformedArgs = [validated, ...args.slice(1)];
130
+ } else if (event === "pong" && args.length >= 1) {
131
+ const data = args[0];
132
+ const validated = this.decodeAndValidate(data, this.schemas.pongSchema);
133
+ transformedArgs = [validated, ...args.slice(1)];
134
+ }
135
+ return transformedArgs;
136
+ }
137
+ /**
138
+ * Wraps an event listener with data transformation logic.
139
+ *
140
+ * This helper intercepts listener registration to inject automatic
141
+ * decoding and validation of incoming event data before passing it
142
+ * to the user's listener function.
143
+ *
144
+ * @param superMethod - The parent class method to call (super.on, super.once, etc.)
145
+ * @param event - The event name
146
+ * @param listener - The user's listener function
147
+ * @returns `this` for method chaining
148
+ *
149
+ * @internal
150
+ */
151
+ wrapListenerWithTransformation(superMethod, event, listener) {
152
+ return superMethod(event, (ws, ...args) => {
153
+ const transformedArgs = this.transformIncomingArgs(event, args);
154
+ return listener(ws, ...transformedArgs);
155
+ });
156
+ }
157
+ on(event, listener) {
158
+ return this.wrapListenerWithTransformation(
159
+ super.on.bind(this),
160
+ event,
161
+ listener
162
+ );
163
+ }
164
+ once(event, listener) {
165
+ return this.wrapListenerWithTransformation(
166
+ super.once.bind(this),
167
+ event,
168
+ listener
169
+ );
170
+ }
171
+ off(event, listener) {
172
+ return this.wrapListenerWithTransformation(
173
+ super.off.bind(this),
174
+ event,
175
+ listener
176
+ );
177
+ }
178
+ addListener(event, listener) {
179
+ return this.wrapListenerWithTransformation(
180
+ super.addListener.bind(this),
181
+ event,
182
+ listener
183
+ );
184
+ }
185
+ removeListener(event, listener) {
186
+ return this.wrapListenerWithTransformation(
187
+ super.removeListener.bind(this),
188
+ event,
189
+ listener
190
+ );
191
+ }
192
+ /**
193
+ * Emits an event with automatic data validation and encoding.
194
+ *
195
+ * All outgoing data is automatically:
196
+ * - Validated against the provided schemas
197
+ * - Encoded from JavaScript objects to Buffer format
198
+ * - Transmitted over the WebSocket connection
199
+ *
200
+ * @param event - The event name to emit
201
+ * @param args - The event arguments (type-checked based on event name)
202
+ * @returns `true` if the event had listeners, `false` otherwise
203
+ *
204
+ * @example Emit a message
205
+ * ```typescript
206
+ * // Data is validated against clientMessages schema and auto-encoded
207
+ * ws.emit('message', { type: 'chat', text: 'Hello!' }, true);
208
+ * ```
209
+ *
210
+ * @example Emit other events
211
+ * ```typescript
212
+ * ws.emit('ping', { timestamp: Date.now() });
213
+ * ws.emit('close', 1000, { reason: 'Normal closure' });
214
+ * ws.emit('error', { code: 500, message: 'Server error' });
215
+ * ```
216
+ */
217
+ emit(event, ...args) {
218
+ let transformedArgs = args;
219
+ if (event === "message" && args.length >= 2) {
220
+ const typedArgs = args;
221
+ const data = typedArgs[0];
222
+ const isBinary = typedArgs[1];
223
+ const clientSchema = this.schemas.clientMessagesSchema;
224
+ if (typeof isBinary === "boolean" && isBinary && clientSchema) {
225
+ const encoded = this.validateAndEncode(
226
+ data,
227
+ clientSchema,
228
+ false,
229
+ "web socket message"
230
+ );
231
+ transformedArgs = [encoded, true, ...typedArgs.slice(2)];
232
+ }
233
+ } else if (event === "close" && args.length >= 2) {
234
+ const typedArgs = args;
235
+ const code = typedArgs[0];
236
+ const reason = typedArgs[1];
237
+ const encoded = this.validateAndEncode(
238
+ reason,
239
+ this.schemas.closeReasonSchema,
240
+ false,
241
+ "web socket close"
242
+ );
243
+ transformedArgs = [code, encoded, ...typedArgs.slice(2)];
244
+ } else if (event === "ping" && args.length >= 1) {
245
+ const typedArgs = args;
246
+ const data = typedArgs[0];
247
+ const encoded = this.validateAndEncode(
248
+ data,
249
+ this.schemas.pingSchema,
250
+ false,
251
+ "web socket ping"
252
+ );
253
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
254
+ } else if (event === "pong" && args.length >= 1) {
255
+ const typedArgs = args;
256
+ const data = typedArgs[0];
257
+ const encoded = this.validateAndEncode(
258
+ data,
259
+ this.schemas.pongSchema,
260
+ false,
261
+ "web socket pong"
262
+ );
263
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
264
+ } else if (event === "error" && args.length >= 1) {
265
+ const typedArgs = args;
266
+ const error = typedArgs[0];
267
+ const errorsSchema = this.schemas.errorsSchema;
268
+ if (errorsSchema) {
269
+ const encoded = this.validateAndEncode(
270
+ error,
271
+ errorsSchema,
272
+ false,
273
+ "web socket error"
274
+ );
275
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
276
+ }
277
+ }
278
+ return super.emit(event, ...transformedArgs);
279
+ }
280
+ // @ts-expect-error - Implementation accepts unknown for internal validation
281
+ send(data, optionsOrCb, cb) {
282
+ const options = typeof optionsOrCb === "function" ? void 0 : optionsOrCb;
283
+ const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
284
+ const encoded = this.validateAndEncode(
285
+ data,
286
+ this.schemas.clientMessagesSchema,
287
+ false,
288
+ "web socket message"
289
+ );
290
+ if (!encoded) {
291
+ throw new Error("Invalid data");
292
+ }
293
+ return super.send(encoded, options || {}, callback);
294
+ }
295
+ /**
296
+ * Closes the WebSocket connection with optional validated close reason.
297
+ *
298
+ * @param code - Optional close code (default: 1000 for normal closure)
299
+ * @param reason - Optional close reason (validated against closeReason schema)
300
+ *
301
+ * @example Close with default code
302
+ * ```typescript
303
+ * ws.close();
304
+ * ```
305
+ *
306
+ * @example Close with code and reason
307
+ * ```typescript
308
+ * ws.close(1000, { reason: 'User logged out' });
309
+ * ```
310
+ *
311
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code|WebSocket Close Codes}
312
+ */
313
+ // @ts-expect-error - Intentionally restricting types for compile-time schema validation
314
+ close(code, reason) {
315
+ if (reason) {
316
+ const encoded = this.validateAndEncode(
317
+ reason,
318
+ this.schemas.closeReasonSchema,
319
+ false,
320
+ "web socket close"
321
+ );
322
+ if (encoded) return super.close(code, encoded);
323
+ }
324
+ return super.close(code);
325
+ }
326
+ /**
327
+ * Sends a ping frame with optional validated data payload.
328
+ *
329
+ * Ping frames are used to check if the connection is alive. The server
330
+ * should respond with a pong frame.
331
+ *
332
+ * @param data - Optional ping data (validated against ping schema)
333
+ * @param mask - Whether to mask the frame (default: true for clients)
334
+ * @param cb - Optional callback invoked when ping is sent or errors
335
+ *
336
+ * @example Send a ping
337
+ * ```typescript
338
+ * ws.ping({ timestamp: Date.now() });
339
+ * ```
340
+ *
341
+ * @example Send ping with callback
342
+ * ```typescript
343
+ * ws.ping({ ts: Date.now() }, true, (error) => {
344
+ * if (error) console.error('Ping failed:', error);
345
+ * });
346
+ * ```
347
+ */
348
+ ping(data, mask, cb) {
349
+ if (data !== void 0) {
350
+ const encoded = this.validateAndEncode(
351
+ data,
352
+ this.schemas.pingSchema,
353
+ false,
354
+ "web socket ping"
355
+ );
356
+ super.ping(encoded, mask, cb);
357
+ } else {
358
+ super.ping(void 0, mask, cb);
359
+ }
360
+ }
361
+ /**
362
+ * Sends a pong frame with optional validated data payload.
363
+ *
364
+ * Pong frames are typically sent in response to ping frames, but can
365
+ * also be sent unsolicited as a unidirectional heartbeat.
366
+ *
367
+ * @param data - Optional pong data (validated against pong schema)
368
+ * @param mask - Whether to mask the frame (default: true for clients)
369
+ * @param cb - Optional callback invoked when pong is sent or errors
370
+ *
371
+ * @example Send a pong
372
+ * ```typescript
373
+ * ws.pong({ timestamp: Date.now() });
374
+ * ```
375
+ *
376
+ * @example Respond to ping
377
+ * ```typescript
378
+ * ws.on('ping', (data) => {
379
+ * console.log('Received ping:', data);
380
+ * ws.pong(data); // Echo the ping data back
381
+ * });
382
+ * ```
383
+ */
384
+ pong(data, mask, cb) {
385
+ if (data !== void 0) {
386
+ const encoded = this.validateAndEncode(
387
+ data,
388
+ this.schemas.pongSchema,
389
+ false,
390
+ "web socket pong"
391
+ );
392
+ super.pong(encoded, mask, cb);
393
+ } else {
394
+ super.pong(void 0, mask, cb);
395
+ }
396
+ }
397
+ };
398
+
399
+ // src/webSocketServer.ts
400
+ import { WebSocketServer } from "ws";
401
+ var ForklaunchWebSocketServer = class extends WebSocketServer {
402
+ /**
403
+ * Creates a new ForklaunchWebSocketServer with schema validation.
404
+ *
405
+ * @param _schemaValidator - The schema validator instance (e.g., ZodSchemaValidator)
406
+ * @param _eventSchemas - Schema definitions for all WebSocket events
407
+ * @param options - Standard WebSocketServer options (port, host, server, etc.)
408
+ * @param callback - Optional callback invoked when the server starts listening
409
+ *
410
+ * @example Create a server on port 8080
411
+ * ```typescript
412
+ * const wss = new ForklaunchWebSocketServer(
413
+ * validator,
414
+ * schemas,
415
+ * { port: 8080 },
416
+ * () => console.log('Server started on port 8080')
417
+ * );
418
+ * ```
419
+ *
420
+ * @example Create a server with existing HTTP server
421
+ * ```typescript
422
+ * import { createServer } from 'http';
423
+ *
424
+ * const httpServer = createServer();
425
+ * const wss = new ForklaunchWebSocketServer(
426
+ * validator,
427
+ * schemas,
428
+ * { server: httpServer }
429
+ * );
430
+ *
431
+ * httpServer.listen(8080);
432
+ * ```
433
+ *
434
+ * @example No server mode (for upgrade handling)
435
+ * ```typescript
436
+ * const wss = new ForklaunchWebSocketServer(
437
+ * validator,
438
+ * schemas,
439
+ * { noServer: true }
440
+ * );
441
+ *
442
+ * httpServer.on('upgrade', (request, socket, head) => {
443
+ * wss.handleUpgrade(request, socket, head, (ws) => {
444
+ * wss.emit('connection', ws, request);
445
+ * });
446
+ * });
447
+ * ```
448
+ */
449
+ constructor(_schemaValidator, _eventSchemas, options, callback) {
450
+ super(options, callback);
451
+ }
452
+ on(event, listener) {
453
+ return super.on(event, listener);
454
+ }
455
+ once(event, listener) {
456
+ return super.once(event, listener);
457
+ }
458
+ off(event, listener) {
459
+ return super.off(event, listener);
460
+ }
461
+ addListener(event, listener) {
462
+ return super.addListener(event, listener);
463
+ }
464
+ removeListener(event, listener) {
465
+ return super.removeListener(
466
+ event,
467
+ listener
468
+ );
469
+ }
470
+ };
471
+
472
+ // index.ts
473
+ import { WebSocket as WebSocket2, WebSocketServer as WebSocketServer2 } from "ws";
474
+ import { WebSocket as WebSocket3 } from "ws";
475
+ import { EventEmitter } from "events";
476
+ var CLOSED = WebSocket3.CLOSED;
477
+ var CLOSING = WebSocket3.CLOSING;
478
+ var CONNECTING = WebSocket3.CONNECTING;
479
+ var OPEN = WebSocket3.OPEN;
480
+ export {
481
+ CLOSED,
482
+ CLOSING,
483
+ CONNECTING,
484
+ EventEmitter,
485
+ ForklaunchWebSocket,
486
+ ForklaunchWebSocketServer,
487
+ OPEN,
488
+ WebSocket2 as WebSocket,
489
+ WebSocketServer2 as WebSocketServer,
490
+ ForklaunchWebSocket as default
491
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@forklaunch/ws",
3
+ "version": "0.1.0",
4
+ "description": "Typed framework for ws, by ForkLaunch.",
5
+ "homepage": "https://github.com/forklaunch/forklaunch-js#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/forklaunch/forklaunch-js/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/forklaunch/forklaunch-js.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Rohin Bhargava",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./lib/index.d.ts",
18
+ "import": "./lib/index.mjs",
19
+ "require": "./lib/index.js",
20
+ "default": "./lib/index.js"
21
+ }
22
+ },
23
+ "types": "lib/index.d.ts",
24
+ "files": [
25
+ "lib/**"
26
+ ],
27
+ "dependencies": {
28
+ "@asyncapi/parser": "^3.4.0",
29
+ "@forklaunch/fastmcp-fork": "^1.0.5",
30
+ "@types/ws": "^8.18.1",
31
+ "ws": "^8.18.3",
32
+ "@forklaunch/common": "0.6.22",
33
+ "@forklaunch/core": "0.16.0",
34
+ "@forklaunch/validator": "0.10.22"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^9.38.0",
38
+ "@typescript/native-preview": "7.0.0-dev.20251027.1",
39
+ "jest": "^30.2.0",
40
+ "prettier": "^3.6.2",
41
+ "ts-jest": "^29.4.5",
42
+ "ts-node": "^10.9.2",
43
+ "tsup": "^8.5.0",
44
+ "typedoc": "^0.28.14",
45
+ "typescript": "^5.9.3",
46
+ "typescript-eslint": "^8.46.2",
47
+ "zod": "^4.1.12"
48
+ },
49
+ "scripts": {
50
+ "build": "tsgo --noEmit && tsup index.ts --format cjs,esm --no-splitting --tsconfig tsconfig.json --outDir lib --dts --clean",
51
+ "clean": "rm -rf lib pnpm.lock.yaml node_modules",
52
+ "docs": "typedoc --out docs *",
53
+ "format": "prettier --ignore-path=.prettierignore --config .prettierrc '**/*.{ts,tsx,json}' --write",
54
+ "lint": "eslint . -c eslint.config.mjs",
55
+ "lint:fix": "eslint . -c eslint.config.mjs --fix",
56
+ "publish:package": "./publish-package.bash",
57
+ "test": "vitest --passWithNoTests"
58
+ }
59
+ }